/* CobbleStone Contract Insight — Blazor shell theme.
   Modern facelift that keeps the recognizable sidebar + top-bar paradigm AND the Contract Insight
   blue brand. Palette matches the legacy CobbleStoneSkin.css: #2C4070 navy is the dominant brand
   color, #0070C0 the bright accent, #455a64 slate, #f5f8fb light surface. */

/* ── Typeface ───────────────────────────────────────────────────────────────────────────────────────
   Inter, self-hosted. Two reasons it replaced the 'Segoe UI', system-ui stack, both functional:
     1. That stack was only actually Segoe on Windows — a Mac got SF, Linux got something else, so the
        app didn't look the same on two desks. One shipped file makes the layout identical everywhere.
     2. Tabular numerals. A contract app is columns of amounts, dates and IDs, and Inter's tnum makes
        digits share one width so they line up in a column instead of dancing row to row.
   Self-hosted rather than a Google/CDN link deliberately: clients run this on-prem and behind proxies
   with no outbound internet, and a webfont that 404s there would silently change every metric on screen.
   The variable file carries the whole 100-900 range in one request, so weights cost nothing extra — and
   it's the full charset, not a latin subset, because the UI is translated (see the localization docs).
   url() is relative to THIS stylesheet, so it resolves under a URL sub-path like every other link. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url(fonts/InterVariable.woff2) format('woff2-variations'), url(fonts/InterVariable.woff2) format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: italic;
    font-weight: 100 900;
    font-display: swap;
    src: url(fonts/InterVariable-Italic.woff2) format('woff2-variations'), url(fonts/InterVariable-Italic.woff2) format('woff2');
}

:root {
    --ci-navy: #2c4070;           /* Contract Insight brand blue (legacy header/nav) */
    --ci-navy-dark: #243358;      /* darker navy for depth */
    --ci-accent: #0070c0;         /* bright accent blue (links/buttons) */
    --ci-accent-dark: #064d80;    /* accent hover */
    --ci-slate: #455a64;          /* secondary text */
    --ci-ai: #5b46c9;             /* VISDOM/AI accent (purple) — the one page-header variation */
    --ci-ai-soft: #efeafc;        /* soft purple fill for AI count/badge chips */
    --ci-ai-on-navy: #cbb8ff;     /* the AI accent lifted for the navy rail — #5b46c9 is unreadable on it */
    --ci-ai-glyph-fill: #8b5cf6;  /* BODY of the AI glyph on the rail; paired with a white outline, see .ci-nav-link-ai svg */

    --ci-sidebar-bg: #2c4070;
    --ci-sidebar-fg: #c7d2e4;
    --ci-sidebar-fg-hover: #ffffff;
    --ci-sidebar-active: #0070c0;
    --ci-topbar-bg: #ffffff;
    --ci-topbar-border: var(--ci-line-strong);
    --ci-content-bg: #f6f7f9;   /* page background */
    --ci-sidebar-w: 248px;
    --ci-sidebar-w-collapsed: 56px;
    --ci-topbar-h: 56px;
    /* The hover ground on the navy rail. One value, no dark-mode pair: the rail is dark in either mode (the
       documented exception in U12), so the ink and this wash don't invert with the theme. */
    --ci-sidebar-hover: rgba(255, 255, 255, .08);
    --ci-ctl-h: 36px;             /* uniform height for every top-bar control (input/buttons/pills) */
    --ci-hdr-icon: 22px;          /* glyph size inside those controls — one var so the breakpoint below scales them all */

    /* OPTICAL sizing for those glyphs. --ci-hdr-icon is the SVG BOX, and an identical box does NOT give an
       identical-looking icon: how much of the 24-unit viewBox a glyph actually draws in varies — the bell fills 19
       units of height, the help square 18. On one box the bell therefore read a size larger than its neighbours
       (reported 2026-08-24). Each glyph takes the box that lands its drawn height on the same ~12.8px (measured in
       the browser, not guessed), and pairs it with the stroke-width that cancels the scaling — stroke-width is in
       VIEWBOX units, so a bigger box also draws a thicker line. Result: one apparent size, one line weight.
       A glyph that needs a THIRD box here probably wants redrawing instead: the header megaphone was scaled up to
       1.31 to match on height, which then left dead space to its right that read as an uneven gap in the row. It
       became a square news sheet and needed no correction at all. */
    --ci-hdr-icon-tall: calc(var(--ci-hdr-icon) * .86);   /* bell — nearly fills the viewBox vertically */
    --ci-hdr-icon-std: calc(var(--ci-hdr-icon) * .95);    /* newspaper, help, shield, lightbulb — ~18 units */

    /* PAGE-CONTENT control height. Deliberately separate from --ci-ctl-h above, which belongs to the app's top bar
       and is scaled by its own breakpoint — the two are unrelated and moving one must not move the other.
       ONE number for a button and for the field beside it, because that is the whole point: a 36px button next to a
       32px picker read as misaligned on every screen that pairs them (measured 2026-08-23 — buttons were 36px while
       every field rendered 28-32px). Change these two values to retune every button and control in the app at once.
         --ci-btn-h     page-header / chrome actions (.ci-hbtn) AND the default form control (.ci-field)
         --ci-btn-h-sm  the compact in-body tier (.ci-btn, .ci-btn-secondary) AND .ci-field-inline beside it
       Row actions (.ci-gbtn, 26px) stay their own tier: they size to the grid row, not to a field.
         --ci-btn-h-xs  the COMPACT tier (.ci-cbtn, 22px) — a card header's own action, where the button is
                        chrome sitting beside a label and a count rather than an action on a row of data. Sized
                        to the text beside it, so a badge or a count reads as its equal instead of its shadow.
                        NOT for anything on a touch surface: 22px is under the 24px target floor, which is the
                        price of the tier and the reason it stays desktop chrome. */
    --ci-btn-h: 32px;
    --ci-btn-h-sm: 30px;
    --ci-btn-h-xs: 22px;

    /* Interface zoom (Account menu → Zoom). Kept in sync with document.documentElement.style.zoom by window.ciScale.
       --ci-vh is "one viewport height, corrected for zoom": under zoom: 0.8, plain 100vh resolves to 80% of the screen
       and leaves a band of empty space at the bottom, so anything meant to fill the window uses var(--ci-vh) instead. */
    --ci-zoom: 1;
    --ci-vh: calc(100vh / var(--ci-zoom));

    /* Card header tint. The same off-white the record field-group headers already use (.dock-head), promoted to a
       variable so every white card can carry the same band instead of being a flat white rectangle. Deliberately NOT
       part of the per-user Appearance theme: that controls brand colour and the grids, and this is structural shading
       that should look the same whatever palette a user picks. The band's height is always the header's own height —
       no fixed value to keep in step, so it comes out naturally smaller on a sidebar widget than on a form.
       Set to #f7faff (2026-08-07, chosen against the live UI). An earlier note here argued for a deeper #eef4fc on
       the grounds that #f7faff sits only ~3% off white and vanished on a laptop panel — that judgement was made
       before the type work, and the band now reads because its heading does more of the work than the fill does.
       Since #f7faff is LIGHTER than the page background, the band no longer separates by being darker than the page;
       it separates by being nearer white than its surroundings, with --ci-card-band-line doing the rest.
       KEEP THE LINE. At this fill strength the 1px edge is not decoration — it is most of the shelf. */
    --ci-card-band: #f8f9fb;
    --ci-card-band-line: #e0e2e7;
    /* The selected tab's fill. It used to just reuse --ci-card-band, which coupled two things that want opposite
       strengths: the band is a passive shelf behind a heading and wants to be barely there, while this is the only
       thing on screen telling you WHICH tab you are on and has to survive being read at a glance. Lightening the
       band to #f7faff proved the coupling was wrong — the tab went faint with it. Separate token, darker on purpose. */
    --ci-tab-active-bg: #e6eefa;
    --ci-ctl-border: var(--ci-line-strong);   /* uniform border for top-bar controls */
    --ci-ctl-bg: #f4f5f7;         /* uniform resting fill for top-bar controls */

    /* ── The top bar's UTILITY GLYPH tier ───────────────────────────────────────────────────────
       A second tier inside the top bar, and the split is about which controls are primary. Five
       icon-only glyphs live up there — notifications, announcements, tips, help, the security
       notice — and as filled bordered pills they read as five buttons demanding attention beside
       the two that genuinely are: "+ New" and the account menu. They are things you reach for
       occasionally, so at rest they are just glyphs, and the pill arrives on hover.
       "+ New", the search button and the Drop-a-document pill KEEP --ci-ctl-bg/--ci-ctl-border.
       That contrast is the whole point — it is now obvious which things in that corner act.
       Each glyph still occupies its full --ci-ctl-h box, so the row's spacing and the notification
       count badges anchored to their corners do not move.
       Consumed by the five components' own scoped stylesheets (NotificationBell, AnnouncementsBell,
       TutorialLauncher, HelpMenu, ConfidentialityNotice) and by .ci-hmenu-icon-btn below — a scoped
       rule out-specifies a global one, so this tier has to be a TOKEN rather than a shared class. */
    --ci-ctl-ghost-bg: transparent;
    --ci-ctl-ghost-bd: transparent;
    --ci-ctl-ghost-fg: var(--ci-slate);
    --ci-ctl-ghost-bg-hover: #e8eef8;

    /* ── Radius scale ───────────────────────────────────────────────────────────────────────────
       FOUR steps and a pill, and every rounded corner in the app is one of them. There is no fifth,
       and no literal px radius anywhere outside this block — a sweep on 2026-08-24 replaced ~1,800
       hardcoded values across 299 files, which is how a corner radius became a decision you make
       once here instead of 1,800 times by hand. Before that sweep eleven different radii were in
       use (2,3,4,5,6,7,8,9,10,12,14px) and two panels side by side rarely agreed.
       What the steps are FOR — pick by the size of the thing, not by taste:
         xs   a chip, a tiny icon button, a progress track — anything under ~20px tall.
         sm   a control: input, button, select, tag, menu item.
         md   a block: a card, a panel, a tab, a popover.
         lg   a large surface: a dialog, a drawer, a full-width section shell.
         pill fully round — a badge or a filter chip whose shape IS its identity. Not a size step.
       The values are deliberately one notch flatter than what they replaced (10px cards → 6px,
       7px controls → 4px). A 10px radius on a 32px control puts a third of each edge into the
       curve, which is the look this app read as "2019 dashboard"; at 4px the edge stays an edge and
       the corner is just softened. If a future retune wants rounder or squarer, it is these five
       numbers — nothing else. Going flatter still (0-2px) needs the borders to carry more, so
       retune --ci-line with it or the surfaces stop separating. */
    --ci-radius-xs: 3px;
    --ci-radius-sm: 4px;
    --ci-radius: 6px;
    --ci-radius-lg: 8px;
    --ci-radius-pill: 999px;

    /* ── Hairlines ──────────────────────────────────────────────────────────────────────────────
       With the radii flattened and the elevation pulled back, the 1px border is doing most of the
       work of separating one surface from the next — so it gets named. --ci-line is the default
       edge for a card or a panel; --ci-line-strong for a control, which has to read as a thing you
       can click and therefore needs a touch more definition than a passive box. */
    --ci-line: #e5e7ea;
    --ci-line-strong: #d6d9de;

    /* ── Elevation ──────────────────────────────────────────────────────────────────────────────
       Two rules, and they are about MEANING, not depth:
         · A surface that sits IN the page (card, panel, grid, tab) gets no lift worth the name —
           it is flat, and its 1px border separates it. The old two-layer --ci-shadow-sm/--ci-shadow
           put a visible drop under every card and button on screen, and a page of forty lifted
           boxes reads as clutter rather than as depth.
         · A surface FLOATING OVER the page (menu, popover, drawer, dialog, toast) keeps a real
           shadow, because there the lift is information: it says this thing is temporary and
           covers what is behind it. That is --ci-shadow-lg, and it deliberately did NOT get
           quieter — flattening it would remove the only cue those layers have. */
    --ci-shadow-sm: 0 1px 1px rgba(16, 24, 40, .04);
    --ci-shadow: 0 1px 2px rgba(16, 24, 40, .06);
    --ci-shadow-lg: 0 10px 28px rgba(16, 24, 40, .10), 0 2px 6px rgba(16, 24, 40, .05);
    --ci-ring: 0 0 0 3px rgba(0, 112, 192, .18);
    --ci-transition: 140ms ease;

    /* ── The hover wash ─────────────────────────────────────────────────────────────────────────
       What a control's background becomes under the pointer. Two steps, and the difference between
       them is not decoration:
         --ci-hover         a control that already has a BORDER (.ci-btn-secondary, .ci-hbtn). The
                            edge is doing the work of saying "this is a thing"; the wash only has to
                            say "…and you're on it".
         --ci-hover-strong  a GHOST control (.ci-grid-tool, .ci-gbtn-icon, the header glyphs). There
                            is no border at rest, so on hover this wash has to draw the button's
                            whole shape, not just tint it — hence a touch deeper.
       Named after a count found 14 different pale blues doing this one job (plus #fdecea, which is
       the separate DANGER hover and stays its own thing). --ci-hover is the plurality value, 35
       sites. The paler tail (#f5f8fb, #f8fafc, #fbfdff, …) is NOT folded in: those sit on already
       tinted surfaces — a striped grid row, a banded card header — where the right wash is a
       different one, and collapsing them would flatten a real distinction rather than a drift. */
    --ci-hover: #f0f1f4;
    --ci-hover-strong: #eaecf0;

    /* ── Motion ─────────────────────────────────────────────────────────────────────────────────
       --ci-transition above is for STATE on something already on screen (a hover, a focus ring).
       These two are for something ARRIVING: a menu, a popover, a dialog.
       One duration and one curve for the whole app, because the point of the entrance is that the
       eye tracks where the thing came from — and that only works if every panel behaves the same.
       140ms is chosen to be under the threshold where an animation starts feeling like a wait: the
       panel is fully readable before you could have moved the pointer into it. Anything at 250ms+
       reads as the app being slow, which is the opposite of polish on a screen you open forty
       times a day.
       The curve is a soft decelerate — fast out of the gate, settling at the end — so the motion
       is over before it draws attention to itself. No bounce, no overshoot: this is a business
       application, and a menu that springs is a menu you notice twice. */
    --ci-enter: 140ms;
    --ci-enter-ease: cubic-bezier(.16, 1, .3, 1);
    /* An in-place content SWAP (a record's tab panel) is slower than an arrival, and deliberately so: a menu is a
       small box in a corner, a tab panel is the whole screen, and 140ms across that much area registers as a flash
       rather than as a transition. Still short enough that a fast clicker never waits on it. */
    --ci-swap: 200ms;
    /* How long a just-changed grid row holds its tint before letting go. Long by the standards of everything else
       here, because this one is not decoration: it is the answer to "which of these fifty rows did I just add?",
       and it has to survive the second or so it takes to look back at the grid after the drawer closes. */
    --ci-flash: 1.6s;

    /* ── Stacking layers ────────────────────────────────────────────────────────────────────────────────────
       One scale, so a new floating thing doesn't have to guess a number and end up fighting an existing one.
         --ci-z-rail    the left sidebar. It ranks above ALL page content, because the collapsed rail's hover
                        flyout overhangs the page and must not be painted over by anything in it — a record's
                        sticky tab strip did exactly that while the rail sat at 2. Note this number is the only
                        one that counts for the flyout: a z-index on the rail makes the whole sidebar one
                        stacking context, so every value inside it is ranked against its rail siblings only.
         --ci-z-navflyout  the flyout INSIDE that context. Small on purpose — it only has to beat the nav links
                        it pops out over, and a big number here would be a lie about what it competes with.
         --ci-z-float   persistent floating affordances that are always on screen (the VISDOM chat launcher and
                        its panel). Deliberately the LOWEST of these: anything the user deliberately opened —
                        a drawer, a dialog, a tour — must cover it, not the other way round.
         --ci-z-scrim   a modal's dimming layer.
         --ci-z-drawer  a slide-in panel, above its own scrim and above anything floating.
         --ci-z-dialog  a centred dialog/prompt — the top of the app's own chrome.
       (Guided tutorials go higher still, at 12000 — see wwwroot/ci-tutorial.css: a tour spotlights the real UI,
       so it has to sit over every layer above, including the Syncfusion dialog stack.)
         --ci-z-toast   the toast stack — over the tutorial layer and everything else the app draws. It
                        used to sit at 1100 and a save made from inside a drawer buried its own confirmation
                        under the scrim. A toast reports what just happened, so it can never be covered.
         --ci-z-working the working bar (ci-busy.js) — the one layer above the toasts, and the only thing on
                        the scale that is not content. It says "your click was received and the server is
                        still on it", which is true of a click made inside a dialog or a drawer just as much
                        as one made on the page, so there is nothing it can be allowed to hide behind. It is
                        2px tall along the top edge and never takes a pointer, so ranking it first costs
                        nothing else. */
    /* ── The page-header gap vs. the sticky tab strip's shield ───────────────────────────────────
       THREE values have to agree here and they live in two different files, which is why the record
       title kept getting its descenders and its ID badge shaved off:
         --ci-scroll-pad   .ci-content's top padding. Content is visible in this band as it scrolls.
         shield            .ci-tabs-wrap::before — an opaque block painted UPWARD from the sticky tab
                           strip to stop that content bleeding over the strip once it pins.
         --ci-phead-gap    the space under a page heading, which is where the shield lands.
       The invariant, and the reason this is tokenised rather than three literals:
           --ci-scroll-pad  ≤  shield  ≤  --ci-phead-gap
       Below the lower bound, content peeks above the pinned strip. Above the UPPER bound, the shield
       paints over the bottom of the page title — there is no z-index escape from that, because the
       strip must out-paint the header for the masking to work at all.
       Raising the header's z-index above the strip is NOT an escape, and it's the first thing you'll
       reach for: the strip has to out-paint the header, because as the header scrolls past a pinned
       strip it would otherwise paint its own text over the tabs — the very bleed the shield prevents.
       There are TWO covers, one per tab-strip variant, and BOTH are sized from --ci-scroll-pad — the
       band of page content they actually have to mask — never from the gap:
         plain  .ci-tabs-wrap::before — height = --ci-scroll-pad.
         docked .ci-tabs-band's box-shadow — a shadow's upward reach is (spread − |offset-y|), which is
                easy to under-read: `0 -1rem 0 1.25rem` reaches 2.25rem up, not 1rem. That is what kept
                eating the record title. It is now split into one tight vertical shadow plus two
                zero-spread lateral copies, so the sideways coverage no longer forces vertical reach.
       That split is what makes the rule simple: the covers take the minimum that does the job, and
       --ci-phead-gap is then a free spacing CHOICE with only one constraint — keep it ≥ --ci-scroll-pad
       and a cover can never reach the title. Tune the gap for looks; touch --ci-scroll-pad only if the
       covers themselves are wrong. (An earlier version sized the plain cover from the gap, so making the
       header roomier silently made its cover taller too, for no reason, and left them exactly flush.) */
    --ci-scroll-pad: .875rem;
    --ci-phead-gap: 1.25rem;

    /* Clearance for the fixed VISDOM chat launcher, bottom-right. Used twice — as the scroller's bottom
       gutter and as a full-height list page's scroll runway — so it is a token, not two literals. */
    --ci-fab-gutter: 4.75rem;
    /* How far the launcher bites into the page HORIZONTALLY: it is 52px wide and sits ~22px off the viewport's
       right edge, so it overhangs the content column's right edge by ~32px. In px, not rem, because the thing it
       is measured against is itself in px — in rem this clearance would shrink on the narrow tier while the
       button it clears stayed the same size. */
    --ci-fab-clear: 44px;

    /* 1180, not 1000: the collapsed rail's flyout overhangs the page, and the rail is the stacking context it is
       trapped in, so the RAIL's number is the only thing that can win. At 1000 a dashboard tile (Syncfusion assigns
       in-page popups z-indexes around 1000 and up) painted straight over the flyout. This sits above every page
       layer and above the floating launcher, and deliberately still BELOW --ci-z-scrim — a modal has to cover the
       shell, nav included. Anything that must overlay the rail belongs at scrim or higher, not at 1181. */
    --ci-z-rail: 1180;
    --ci-z-navflyout: 10;
    --ci-z-float: 1050;
    /* A popover anchored to something that ISN'T an element — the placeholder type-ahead, which hangs off the text
       caret and so has to be appended to <body> rather than rendered in place. Above page content and above
       Syncfusion's in-page popups (which assign themselves 1000 and up), and below the scrim for the same reason
       the rail is: a modal covers everything the page owns. */
    --ci-z-anchored: 1190;
    --ci-z-scrim: 1200;
    --ci-z-drawer: 1205;
    --ci-z-dialog: 1400;
    --ci-z-toast: 12500;
    --ci-z-working: 12600;

    /* ── Type scale ─────────────────────────────────────────────────────────────────────────────
       Five steps, and every heading and label in the app is one of them. Before this each component
       invented its own size/weight/tracking/colour, and the drift was visible: a sidebar widget's
       heading ("Contract Summary") ended up SMALLER than, and the same grey as, the field labels it
       governs — a heading rendering quieter than its own content. What the steps mean:
         page     the one title on the screen.
         section  a card or field-group heading ("Details") — Title Case, never caps.
         panel    a sidebar widget's heading. The section step one notch down, so the right-hand
                  column rhymes with the middle one instead of looking like a different app.
         label    what NAMES a value: field labels, nav section dividers. The ONLY use of caps left.
         meta     supporting counts and timestamps sitting beside any of the above.
       The point of the split is that CAPS now means exactly one thing — "this names a value" — so a
       heading and a field label can never again be told apart only by 0.1rem of size.
       Sizes are ~a notch tighter than the Segoe values they replace: Inter has a taller x-height and
       renders about 9% larger at the same nominal size, which the smallest steps had to absorb. */
    /* The app typeface. Everything that draws text points at this one token — including the Syncfusion
       override at the foot of this file — so the face is changed in exactly one place. The fallbacks
       matter: they are what renders during the swap, and on a locked-down client where the woff2 can't
       be fetched at all, and they're ordered so that stack stays closest to Inter's metrics. */
    --ci-font: 'Inter', 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;

    --ci-fs-page: 1.55rem;    --ci-fw-page: 700;     --ci-ls-page: -.011em;
    --ci-fs-section: .95rem;  --ci-fw-section: 700;  --ci-ls-section: -.003em;
    --ci-fs-panel: .82rem;    --ci-fw-panel: 700;    --ci-ls-panel: .005em;
    /* Weight 500, not 600, and slightly less tracking. A record view puts ~20 of these caps labels on screen at
       once and at 600 they visually outweighed the VALUES beside them — which are the thing people came to read.
       The colour deliberately did NOT get lighter: --ci-ink-label on white is already around 3:1, so buying
       quietness with more contrast loss would cost legibility. Weight is the free axis here. */
    --ci-fs-label: .72rem;    --ci-fw-label: 500;    --ci-ls-label: .04em;
    --ci-fs-meta: .74rem;     --ci-fw-meta: 500;

    /* Heading + label inks. #8ea0c2 was pasted into ~30 rules as "the quiet label blue-grey"; naming it
       is what lets a future palette change reach all of them at once. */
    /* Page + section headings. Points at --ci-brand-ink rather than --ci-navy directly so the
       headings can lift in dark mode without decoupling from the user's chosen brand colour — in
       light this still resolves to exactly var(--ci-navy). See --ci-brand-ink below. */
    --ci-ink: var(--ci-brand-ink);
    --ci-ink-2: #455a64;          /* panel headings — present, but not competing with the section above */
    --ci-ink-label: #8ea0c2;      /* field labels + nav dividers */
    --ci-ink-meta: #8ea0c2;       /* counts, timestamps, "24 fields" */
    /* An empty widget/panel says its message in this — quieter than a label, never invisible. */
    --ci-ink-quiet: #7c8ba1;

    /* ══ THE SEMANTIC LAYER ═════════════════════════════════════════════════════════════════════
       Everything above this line is the app's LIGHT palette and always was. The block below is what
       makes a second palette possible at all, and it exists because of a count: a survey on
       2026-08-24 found 6,973 colour literals in CSS position across 288 style blocks and 45
       stylesheets, resolving to 298 distinct text colours, 434 surfaces and 303 hairlines. Not one
       of them could follow a mode, and no amount of care in a page's own <style> block can fix that
       — a literal is by definition the same colour in every mode.

       The literals were almost perfectly ROLE-PURE, which is what made this tractable: of 264 uses
       of #455a64, 252 were `color:` and one was a border. Nobody was using one grey for two jobs.
       They were each inventing a new grey for the SAME job, which means the fix is naming the job.

       So: pick by ROLE, never by how the colour looks. The name is the contract — a token used for
       the job it names is correct in every mode for free, and one used for a job it doesn't name
       breaks the moment a second mode exists. The dark values live in the two blocks after this
       rule set; add a token HERE and you must add it THERE, or dark mode inherits a light colour
       and you get white-on-white.

       WHAT IS NOT HERE, deliberately:
         · The brand colours (--ci-navy/--ci-accent). Those are the USER's, set per person on
           Account → Appearance, and they don't invert — see --ci-brand-ink below for the half
           that has to.
         · Chart and status colours living in C# (LaunchpadTileService, TagCloudService,
           WorkflowFlowService, TaskStatusService). CSS variables can't reach a server-rendered
           palette; those read the mode from IColorModeService instead.
         · Anything on --ci-paper. See that token. */

    /* ── Surfaces ────────────────────────────────────────────────────────────────────────────────
       Four steps, and the step is chosen by how DEEP the thing sits, not by how light it looks:
         --ci-surface       a card, a panel, the top bar. The default place content lives.
         --ci-surface-2     something raised or nested ON a surface — an inner card, a striped row.
         --ci-surface-sunk  something INSET into a surface: a well, a code block, an empty state.
       --ci-content-bg (the page itself, further up) is the fourth and was already named.
       In light these are near-white and separated by ~2%; in dark they are separated by ~6%,
       because a 2% step that reads fine on white vanishes on near-black. */
    --ci-surface: #ffffff;
    --ci-surface-2: #f9fafb;
    --ci-surface-sunk: #f6f7f9;

    /* ── Text ────────────────────────────────────────────────────────────────────────────────────
       --ci-ink / -2 / -label / -meta / -quiet are declared above and keep their meanings. These two
       close the ladder at both ends:
         --ci-ink-body      the value you came to read — a grid cell, a field's value, body copy.
                            Darker than --ci-ink-2, which is for a panel HEADING over it.
         --ci-ink-faint     the quietest legible tier: placeholders, disabled text, a timestamp
                            beside something else. Below --ci-ink-quiet, above invisible.
       --ci-ink-on-brand is text on a FILLED brand surface (a primary button, the nav rail). It does
       not invert: white on the brand colour is white in both modes, and expressing that as a token
       is what stops a sweep from "helpfully" darkening it. */
    --ci-ink-body: #263238;
    --ci-ink-faint: #90a4ae;
    --ci-ink-on-brand: #ffffff;

    /* Two more rungs, and they exist because the sweep measured them rather than because a ladder
       wants to be symmetrical. The app grew TWO grey families — Material blue-grey (#455a64,
       #607d8b, #90a4ae) and Tailwind slate (#64748b, #94a3b8, #cbd5e1) — from being built by
       different hands at different times. Collapsing them onto four rungs would have moved 373
       sites of secondary text a visible step darker, so light mode keeps the distinction and DARK
       mode collapses it: both -2 and -support resolve to one neutral there. That is the whole
       reason light mode comes out of this sweep pixel-identical.
         --ci-ink-support   between secondary and quiet. ~4.8:1 on white — still clear of AA.
         --ci-ink-disabled  deliberately BELOW the legibility floor: a disabled control's label, a
                            placeholder. Never use it for text somebody has to read. */
    --ci-ink-support: #64748b;
    --ci-ink-disabled: #b0bec5;

    /* A row divider INSIDE a panel — quieter than --ci-line, which is the panel's own edge. Named
       because 23 rules had reached for #f1f5f9 to get it. */
    --ci-line-faint: #f2f3f5;

    /* ── Brand, as text rather than as a fill ────────────────────────────────────────────────────
       THE coupling that makes or breaks dark mode, and we had already hit it once: --ci-ai-on-navy
       exists because the VISDOM purple #5b46c9 is unreadable on the navy rail. Dark mode is that
       same problem applied to the whole app.

       --ci-navy is a fine BACKGROUND in either mode — it's a dark fill on a dark ground, which is
       just a nav rail. As TEXT on a dark ground it is invisible, and we cannot substitute a better
       constant because the colour is the user's own pick from Account → Appearance.

       So the pair is split: --ci-navy stays the fill, --ci-brand-ink is the text. The value comes
       from ThemeService, which already does the WCAG math that keeps a light custom primary legible
       on white — it now runs that math twice and emits BOTH answers (--ci-brand-ink-light and
       --ci-brand-ink-dark) into the saved palette. The fallbacks below are what an untouched user
       gets, so a palette saved before this shipped still resolves. */
    --ci-brand-ink: var(--ci-brand-ink-light, var(--ci-navy));
    --ci-accent-ink: var(--ci-accent-ink-light, var(--ci-accent));
    --ci-ai-ink: var(--ci-ai);

    /* The brand as an EMPHASIS EDGE — the third member of this family, and it exists for the same
       reason as the other two. 25 rules said `border-color: var(--ci-line-emphasis)` to mean "you are hovering
       this" or "this one is selected". A dark navy edge does that job on a white card and disappears
       entirely on a dark one, so the strongest signal in a list silently stops working.
       Light resolves to exactly var(--ci-navy) — unchanged — and dark takes the lifted brand ink. */
    --ci-line-emphasis: var(--ci-navy);

    /* ── Status ──────────────────────────────────────────────────────────────────────────────────
       Three families × ink / fill / edge. The light values consolidate what was already in use
       (#2e7d32 #2e7d43 #166534 #16a34a were all "success text"); the dark inks are LIFTED and the
       dark fills are genuinely dark. That second half is the one people get wrong — a pale green
       chip keeps its pale green fill, the ink on it goes light, and the result is unreadable. A
       status fill is a tint of its ink against the SURFACE, so it has to travel with the surface.

       Each light value is the PLURALITY literal of its family, not a fresh colour picked to look nice
       between them: #b3261e was 66 sites of danger text against #c0392b's 55, so taking #b3261e
       leaves the larger half pixel-identical and moves the smaller half by a step nobody can see.
       Inventing a midpoint would have moved BOTH halves. */
    --ci-ok: #2e7d32;
    --ci-ok-surface: #e6f4ea;
    --ci-ok-line: #bfe0ca;
    --ci-warn: #92400e;
    --ci-warn-surface: #fdf0d5;
    --ci-warn-line: #fde68a;
    --ci-danger: #b3261e;
    --ci-danger-surface: #fdecea;
    --ci-danger-line: #f0b4ae;
    /* Document-compare redline (RainbowMergeEngine's <ins>/<del>). The light values ARE the engine's own inline
       colours, so light mode is unchanged; the dark block gives them readable counterparts (bug 45276). */
    --ci-diff-ins-bg: SpringGreen;
    --ci-diff-ins-ink: var(--ci-ink-body);
    --ci-diff-del-ink: red;

    /* ── Module accents ──────────────────────────────────────────────────────────────────────────
       One colour per module, so a contract is the same navy wherever it is summarised — the Home
       stat tiles and My Favorites both draw from this list, and they were each carrying their own
       copy of the twelve literals.

       These are IDENTITY, not status: the colour says "this is Solicitations", it never says good or
       bad. That is why they are their own family rather than borrowed from --ci-ok / --ci-warn.

       They appear as a 4px bar down a card's edge and as a faint icon watermark — a FILL and a
       glyph, never text — so they are held to the 3:1 non-text bar rather than the text ladder.
       The light values are the exact literals both widgets already used, so light mode does not
       move by a pixel. The dark values are LIFTED: every one of these is a mid-to-dark colour
       chosen against white, and on #161e2b the navy and the slate in particular went from "quiet"
       to gone — a tile whose accent bar has vanished reads as a tile that failed to load.
       Lifting keeps the family recognisable (the navy stays the blue-violet end, the slate stays
       the neutral) instead of substituting six colours that happen to be visible. */
    --ci-mod-contracts: #2c4070;
    --ci-mod-requests: #0070c0;
    --ci-mod-solicitations: #2e9e5b;
    --ci-mod-purchaseorders: #7c3aed;
    --ci-mod-vendors: #0891b2;
    --ci-mod-employees: #455a64;
    --ci-mod-clauses: #6d28d9;
    --ci-mod-tasks: #b45309;
    --ci-mod-surveys: #be185d;
    --ci-mod-matrices: #0f766e;
    --ci-mod-reports: #0369a1;
    /* Anything with no accent of its own. Deliberately the same as Employees: a neutral slate is what
       "no particular colour" looks like, and inventing a thirteenth hue for it would imply identity. */
    --ci-mod-default: var(--ci-mod-employees);

    /* What dims the page behind a modal. Was a literal rgba in a dozen places; in dark it has to go
       much deeper, because a 45%-black veil over an already dark page barely reads as a veil. */
    --ci-scrim: rgba(31, 43, 61, .45);

    /* ── The elevation hairline ──────────────────────────────────────────────────────────────────
       Transparent in light, a real 1px line in dark, and that asymmetry is the whole point.
       Elevation in light is a soft dark shadow under the element. On a dark ground that shadow is
       invisible — black on near-black — and the usual response is to crank the alpha, which is
       exactly what makes an app's dark mode look cheap and smudged. Dark elevation is a LIGHTER
       surface plus a light hairline along its top edge, which is how a raised thing catches light.
       One token lets a single `box-shadow` declaration be correct in both modes. */
    --ci-elev-line: transparent;

    /* ── Paper ───────────────────────────────────────────────────────────────────────────────────
       The deliberate NON-inverting pair, and a ratified decision rather than an unfinished edge:
       a contract, an email body, a merged template and a report are DOCUMENTS. A document is white,
       in both modes, the same way it is white in Word and white when printed. In dark mode these
       read as a lit page on a dark desk, which is the correct treatment and not a compromise.
       Recolouring correspondence would also misrepresent what the sender actually sent.
       These exist as tokens so "stays light" is one declaration somebody can find and argue with,
       instead of forty literals that merely look like a sweep that ran out of steam.
       Consumers: CiFilePreview, RecordEmails, SecureEmailThreadView, NotificationTemplatesAdmin,
       NotificationThemesAdmin, DocTemplateFileEdit, VisdomWorkbench, and the Stimulsoft viewers. */
    --ci-paper: #ffffff;
    --ci-paper-ink: #1f2b3d;
}

/* ══ DARK MODE ══════════════════════════════════════════════════════════════════════════════════
   TWO states, and light is the absence of the attribute:
     ·  no data-theme        →  light. The default, and what every user who never opens
                                Account → Appearance keeps getting.
     ·  data-theme="dark"    →  dark.
   The attribute is stamped on <html> SERVER-SIDE in App.razor, from t_UserPrefs, so it is in the
   first byte of HTML — the same reason the palette blocks are rendered there rather than applied by
   script. A client-side stamp would paint the light palette first and then visibly flip it.

   There is no workspace dark default: Administration → Branding sets the PALETTE, never the mode.
   Putting 400 people into dark mode because an administrator liked it is the same "don't repaint
   someone's app for them" objection that made appearance per-user in the first place.

   A THIRD state — data-theme="system", following prefers-color-scheme — was built and then removed
   (2026-08-24, user's call: the choice is light or dark, explicitly). Worth knowing why the removal
   was a win beyond the simpler screen: that state needed its own @media (prefers-color-scheme: dark)
   copy of EVERY token in the block below, and a token added to one copy and not the other silently
   keeps its light value in dark mode. That trap had already fired once (--ci-ink-support,
   --ci-ink-disabled and --ci-ai-soft shipped light-only and put count chips at 2.95:1). One block
   cannot drift from itself. Don't reintroduce the media query without solving that.

   The block below redefines ONLY tokens. No component rule may live in here: a colour whose only
   definition sits inside a [data-theme] block does not exist in the light state, which is the
   classic half-themed-page bug. If dark needs a colour, it needs a TOKEN. */

:root[data-theme="dark"] {
    /* Surfaces. Steps ~6% apart, not the 2% that works on white — see --ci-surface above. */
    --ci-content-bg: #111317;
    --ci-surface: #1a1e24;
    --ci-surface-2: #22272e;
    --ci-surface-sunk: #0d0f13;
    --ci-topbar-bg: #161e2b;
    --ci-card-band: #20242b;
    --ci-card-band-line: #2e343c;
    --ci-tab-active-bg: #24304a;
    --ci-ctl-bg: #262a31;
    --ci-ctl-ghost-bg-hover: #28344a;

    /* Text. --ci-ink is the brand ink, so it follows the user's palette via --ci-brand-ink-dark. */
    --ci-ink: var(--ci-brand-ink);
    --ci-ink-body: #d5deec;
    --ci-ink-2: #b3c0d4;
    --ci-ink-label: #8b99b0;
    --ci-ink-meta: #8b99b0;
    --ci-ink-quiet: #8b99b0;
    --ci-ink-faint: #6a7890;
    /* Added after a dark-mode contrast audit caught the count chips at 2.95:1: these two were declared in
       the light block and NOT here, so they kept their LIGHT values in dark mode — exactly the trap the
       header warns about. A token added above MUST be added in both blocks below. */
    --ci-ink-support: #9fadc2;
    --ci-ink-disabled: #5a6779;
    /* The AI wash. Pale lilac in light; a dark plum here, so a lifted --ci-ai-ink reads on it. */
    --ci-ai-soft: #241d3d;
    --ci-slate: #b3c0d4;

    /* Hairlines. */
    --ci-line-faint: #262b32;
    --ci-line: #2b3038;
    --ci-line-strong: #3d434c;

    /* Brand as text — lifted. The -dark fallbacks cover a palette saved before this shipped and the
       built-in navy, neither of which carries a computed value. */
    --ci-brand-ink: var(--ci-brand-ink-dark, #8fa5d6);
    --ci-accent-ink: var(--ci-accent-ink-dark, #4fa8e8);
    --ci-ai-ink: var(--ci-ai-on-navy);
    --ci-line-emphasis: var(--ci-brand-ink);
    --ci-sidebar-fg: #b8c6de;

    /* The nav RAIL, lifted. Every preset primary is a dark colour designed for a WHITE page, so reused as-is
       on a dark page the rail stops being a region — Midnight measured 1.27:1 against --ci-content-bg, and at
       that value the shell is one flat dark expanse with a hairline in it. ThemeService derives the lift
       (--ci-navy-on-dark) holding hue and saturation, so "Midnight" still reads as midnight. The fallbacks are
       the built-in palette UNCHANGED, and that is correct rather than lazy: #2c4070 measures 0.054 relative
       luminance, already clear of the 0.045 floor, so the default rail needs no lift and picking the default
       preset (which deletes the stored palette, so nothing is emitted) must leave it exactly as it was.
       NOTE this deliberately moves --ci-navy itself, not just the sidebar: --ci-navy is the brand FILL, and a
       brand fill wants the same lift anywhere it lands on a dark ground (the grid header, a filled badge). */
    --ci-navy: var(--ci-navy-on-dark, #2c4070);
    --ci-navy-dark: var(--ci-navy-dark-on-dark, #243358);
    --ci-sidebar-bg: var(--ci-navy-on-dark, #2c4070);

    /* Status: inks lift, fills go dark. */
    --ci-ok: #6fcf8b;
    --ci-ok-surface: #16281c;
    --ci-ok-line: #2c4a36;
    --ci-warn: #e0a657;
    --ci-warn-surface: #2b2113;
    --ci-warn-line: #4a3a1c;
    --ci-danger: #f08a82;
    --ci-danger-surface: #2c1614;
    --ci-danger-line: #5a2a26;
    /* Light body text on SpringGreen was unreadable: a deep green fill with pale text, and the lifted danger ink. */
    --ci-diff-ins-bg: #2e6b44;
    --ci-diff-ins-ink: #eaf7ee;
    --ci-diff-del-ink: #f08a82;

    /* Module accents, lifted. Same twelve identities, each raised until the 4px bar reads as a colour
       against --ci-surface (#161e2b) rather than as a slightly-different dark edge. Two reuse a token
       the app already lifted for the same hue — Requests takes the dark accent ink, Tasks the dark
       warn — so an amber stays one amber across the whole dark palette. --ci-mod-default is not
       repeated: it points at Employees in both modes and follows it here for free. */
    --ci-mod-contracts: #7d97d4;
    --ci-mod-requests: #4fa8e8;
    --ci-mod-solicitations: #5ec98a;
    --ci-mod-purchaseorders: #a78bfa;
    --ci-mod-vendors: #3fbcd6;
    --ci-mod-employees: #90a4ae;
    --ci-mod-clauses: #9575e8;
    --ci-mod-tasks: #e0a657;
    --ci-mod-surveys: #f472a8;
    --ci-mod-matrices: #3fb8ab;
    --ci-mod-reports: #6fb3dd;

    /* Elevation: the shadow deepens AND the top hairline arrives — see --ci-elev-line. */
    --ci-shadow-sm: 0 1px 1px rgba(0, 0, 0, .40);
    --ci-shadow: 0 1px 2px rgba(0, 0, 0, .50);
    --ci-shadow-lg: 0 10px 28px rgba(0, 0, 0, .55), 0 2px 6px rgba(0, 0, 0, .40);
    --ci-elev-line: #39465a;
    --ci-ring: 0 0 0 3px rgba(79, 168, 232, .35);
    --ci-scrim: rgba(3, 6, 12, .66);

    /* Hover washes lighten instead of darkening — on a dark surface "more light" is what reads as
       "you are on it". Inverting these is the single most common miss in a hand-built dark mode. */
    --ci-hover: #262b33;
    --ci-hover-strong: #2e343e;

    color-scheme: dark;
}

/* One helper per step, so new markup has a single thing to reach for instead of inventing a sixth
   variant. Existing components consume the vars directly (below) rather than being re-classed. */
.ci-t-page { font-size: var(--ci-fs-page); font-weight: var(--ci-fw-page); letter-spacing: var(--ci-ls-page); color: var(--ci-ink); line-height: 1.25; }
.ci-t-section { font-size: var(--ci-fs-section); font-weight: var(--ci-fw-section); letter-spacing: var(--ci-ls-section); color: var(--ci-ink); }
.ci-t-panel { font-size: var(--ci-fs-panel); font-weight: var(--ci-fw-panel); letter-spacing: var(--ci-ls-panel); color: var(--ci-ink-2); }
.ci-t-label { font-size: var(--ci-fs-label); font-weight: var(--ci-fw-label); letter-spacing: var(--ci-ls-label); color: var(--ci-ink-label); text-transform: uppercase; }
.ci-t-meta { font-size: var(--ci-fs-meta); font-weight: var(--ci-fw-meta); color: var(--ci-ink-meta); }

/* Digits that line up. Scoped to the surfaces that actually hold columns of numbers — grids, record
   values, summary tags, stat tiles — rather than set globally, because tabular figures are wider and
   slightly airier, which is right for a column of money and wrong for a sentence of prose. */
.e-grid, .record-field, .rs-tag-val, .ci-stat-value, .ci-stat-num, .dock-head-n, .rs-wcount, .rs-pill-n {
    font-variant-numeric: tabular-nums;
}

/* The typeface is set on BODY, not only on .ci-shell: Syncfusion renders dialogs, dropdown popups and
   tooltips into a portal at body level, outside the shell, so a font set on the shell alone left every
   popup in the browser default — two fonts on one screen the moment you opened a picker. */
body {
    margin: 0 !important;
    font-family: var(--ci-font);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* An explicit ground. The page used to inherit white from the UA, which is invisible until there is a
       second mode — then the shell goes dark and any gap between panels flashes white behind it. */
    background: var(--ci-content-bg);
    color: var(--ci-ink-body);
}

/* ── THE TYPEFACE HAS TO REACH FORM CONTROLS TOO ─────────────────────────────────────────────────────────
   A <button>, <input>, <select> or <textarea> does NOT inherit font-family — the UA hands it one of its own
   (Arial, on Chrome/Windows), and nothing in this sheet was handing it back. Every button tier here sets
   font-SIZE and font-WEIGHT but never the family, so the app's buttons were drawing in Arial while the text
   around them drew in Inter.

   That is invisible until the two sit side by side, which is how it was found: Package Manager's header puts
   an <a class="ci-hbtn">Download</a> (an anchor, which DOES inherit) next to <button class="ci-hbtn">Make
   inactive</button>, and the two rendered in different typefaces. The same pairing exists on ~40 screens —
   every download link, every "Choose file…" span — so this is one rule here rather than a patch per page.

   FAMILY ONLY. Size and weight stay with the tier classes, which set them deliberately and per tier. An
   element selector weighs (0,0,1), so any component that declares its own family — Syncfusion's e-icons spans,
   the monospace blocks — still wins without an !important anywhere. */
button, input, select, textarea, optgroup { font-family: inherit; }

/* ── Bare form controls ──────────────────────────────────────────────────────────────────────────────────
   A whole CLASS of dark-mode bug, found by looking at Administration: `.ah-search` declares a border, a
   height and a radius but no BACKGROUND, because in light mode the browser's own white was already right.
   Under a dark shell that control renders in the UA's dark-form colour — a muddy near-brown that matches
   nothing else on the page. There are bare <input>/<select>/<textarea> elements across ~199 components, so
   this is fixed once here rather than per screen.

   Mode-agnostic on purpose: in light these tokens resolve to the white and near-black the UA was already
   giving us, so this changes nothing today and cannot drift from the dark values tomorrow.

   Syncfusion's own controls are EXCLUDED. They paint their internals from their stylesheet (which we swap
   wholesale for bootstrap5-dark.css), and a global rule here would fight the vendor's theme on every input,
   dropdown and date picker — the kind of specificity war that ends in !important. Their markup is reliably
   `class="e-input"` / `e-control`, hence the attribute guard.

   The specificity note that used to be here was WRONG, and knowing so is worth the two lines: `:not()` contributes
   the specificity of its own argument, so this chain of eight attribute guards weighs (0,8,1) — nine classes deep.
   A component that sets its own background does NOT win and cannot reasonably be made to (measured 2026-09-06: an
   input on the navy rail came out white, with dark text on a dark ground). If you ever need a field painted by the
   box AROUND it rather than by this baseline, add a guard here for it; do not go looking for a selector long
   enough to outrank this one. */
input:not([class*="e-input"]):not([class*="e-control"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]):not([type="range"]):not([type="file"]):not([type="submit"]):not([type="button"]),
textarea:not([class*="e-input"]):not([class*="e-control"]),
select:not([class*="e-input"]):not([class*="e-control"]) {
    background: var(--ci-surface);
    color: var(--ci-ink-body);
}

/* A placeholder is not body text — it names what to type and must stay quieter than a real value, or an
   empty field reads as a filled one. */
input::placeholder,
textarea::placeholder {
    color: var(--ci-ink-faint);
    opacity: 1;   /* Firefox dims placeholders again on top of the colour */
}

.ci-shell {
    display: flex;
    height: var(--ci-vh);   /* bound to the viewport (zoom-corrected) so the sidebar + topbar stay fixed */
    overflow: hidden;       /* and only .ci-content scrolls (not the whole page) */
    font-family: var(--ci-font);
}

/* Sidebar */
.ci-sidebar {
    width: var(--ci-sidebar-w);
    background: linear-gradient(180deg, var(--ci-navy) 0%, var(--ci-navy-dark) 100%);
    color: var(--ci-sidebar-fg);
    display: flex;
    flex-direction: column;
    transition: width var(--ci-transition);
    flex-shrink: 0;
    box-shadow: 2px 0 8px rgba(16, 24, 40, .10);
    /* A z-index on the rail makes the WHOLE sidebar one stacking context, so every number inside it — including the
       collapsed rail's hover flyout at --ci-z-navflyout — is ranked only against its siblings here, not against the
       page. At 2 the record screen's sticky tab strip (z-index 6, inside .ci-main) painted straight over the flyout.
       The rail is app chrome and its flyout overhangs the page, so it ranks above page content as a whole. */
    z-index: var(--ci-z-rail);
}
.ci-sidebar.collapsed { width: var(--ci-sidebar-w-collapsed); }

.ci-brand {
    height: var(--ci-topbar-h);
    display: flex;
    align-items: center;
    gap: .5rem;
    /* 8px, not the .75rem the rest of the rail uses: it is what puts the stones mark's centre on 28px in both
       rail widths (see .ci-brandbtn). A rem here would move the mark at the responsive tiers. */
    padding: 0 8px;
    border-bottom: 1px solid rgba(255,255,255,.08);
}
.ci-brand-text { font-weight: 700; color: var(--ci-ink-on-brand); white-space: nowrap; font-size: 1.05rem; }

/* ── The brand lockup, which is also the rail's collapse control ──────────────────────────────────────────
   One rule does the whole trick: the stones mark is 40px wide with 8px of padding beside it, so its centre is
   at 28px — exactly half of the 56px collapsed rail, and exactly the centre of the collapsed nav icons below
   it. Collapsing therefore takes the wordmark away and leaves the mark where it already was, in both states,
   with nothing swapped in its place. Sizes are fixed px on purpose: this is one piece of artwork that has to
   land on the same pixel at every responsive tier, so it must not scale with the root font size. */
.ci-brandbtn {
    position: relative; display: flex; align-items: center; gap: 9px;
    width: 100%; height: 100%; padding: 0; background: transparent; border: 0;
    cursor: pointer; overflow: hidden; border-radius: var(--ci-radius);
    transition: background var(--ci-transition);
}
.ci-brandbtn:hover { background: rgba(255,255,255,.06); }
.ci-brandbtn:focus-visible { outline: 2px solid rgba(255,255,255,.65); outline-offset: -2px; }
.ci-brand-mark { width: 40px; height: auto; flex: none; transition: opacity var(--ci-transition); }
/* 14px, and the number is DERIVED rather than picked — see below. Do not "round it up to look bolder".
   The 2026 wordmark is ONE line of bold caps where the 2025 one stacked COBBLESTONE over "software", so its
   aspect ratio doubled: 9.41:1 against the old 4.51:1. At the old height: 22px that renders 207px wide, which
   overflows the 232px available inside the rail once the 40px mark, two 9px gaps and the chevron are in — the
   wordmark ran off the edge and read as enormous.
   The right height comes from the master logo's own proportion (CS-Blue-Horizontal-Logo-2026.svg): its
   wordmark is 114.99 units tall against the stones' 172.74, i.e. 0.666. The mark renders 20.8px tall at its
   fixed 40px width, so 20.8 x 0.666 = 13.9px. At 14px the wordmark is 131px wide and the whole lockup lands
   at ~189px inside 232px, with the same cap height the 2025 lockup had. */
/* Half a step back from the stones, on purpose. The mark is the thing you recognise at a glance and the one
   that survives collapsing; the word spells out what the mark already said. At full white the bold 2026
   wordmark out-shouted it, because bold caps cover far more area than the stones do at the same height.
   .9 is deliberately small — on the navy rail it resolves to about #e9ebf0, so it still measures ~9:1 and
   reads as "quieter", not "faded". Anything under about .85 starts looking like a loading state.
   Applied here rather than in the SVG's fill so the asset stays canonical white and reusable, and so this is
   one number to tune. */
.ci-brand-word { height: 14px; width: auto; flex: none; opacity: .9; }
/* The affordance a logo can't carry on its own: which way the rail is about to move. Hidden until the pointer
   or the keyboard is actually on the control, so it never competes with the logo it sits next to. */
.ci-brand-chev {
    display: inline-flex; align-items: center; margin-left: auto; padding-right: 2px;
    color: var(--ci-sidebar-fg); opacity: 0; transition: opacity var(--ci-transition);
}
.ci-brandbtn:hover .ci-brand-chev,
.ci-brandbtn:focus-visible .ci-brand-chev { opacity: 1; }
/* Collapsed there is no room beside the mark, so the chevron takes the mark's place on hover instead. */
.ci-brand-chev.collapsed { position: absolute; inset: 0; margin: 0; padding: 0; justify-content: center; }
.ci-sidebar.collapsed .ci-brandbtn:hover .ci-brand-mark,
.ci-sidebar.collapsed .ci-brandbtn:focus-visible .ci-brand-mark { opacity: .18; }

.ci-nav { padding: .5rem 0; overflow-y: auto; flex: 1; }
/* A nav divider names a group of destinations, which is the same job as a field label — so it's the label
   step, and keeps its caps. The colour is the label ink, which already reads correctly on the navy rail. */
.ci-nav-section { padding: .6rem .9rem .25rem; text-transform: uppercase; color: var(--ci-ink-label);
    font-size: var(--ci-fs-label); font-weight: var(--ci-fw-label); letter-spacing: var(--ci-ls-label); }
.ci-nav .active { background: rgba(255,255,255,.10); color: var(--ci-ink-on-brand); box-shadow: inset 3px 0 0 var(--ci-accent); }
.ci-nav-link {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; box-sizing: border-box;
    padding: .55rem .9rem; color: var(--ci-sidebar-fg);
    text-decoration: none; background: transparent; border: 0;
    font-size: .92rem; cursor: pointer; text-align: left; gap: .5rem;
    transition: background var(--ci-transition), color var(--ci-transition), box-shadow var(--ci-transition);
}
.ci-nav-link:hover { background: rgba(255,255,255,.08); color: var(--ci-sidebar-fg-hover); }
.ci-nav-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ci-nav-caret { font-size: .7rem; opacity: .7; }
.ci-nav-children { background: rgba(0,0,0,.18); }
.ci-nav-children .ci-nav-link { padding-left: 1.6rem; font-size: .88rem; }
.ci-nav-disabled { opacity: .4; cursor: not-allowed; }
.ci-nav-disabled:hover { background: transparent; color: var(--ci-sidebar-fg); }

/* Port-status markers on nav items: ✓ = ported to the new app, ↗/● = legacy / not yet migrated. */
.ci-nav-badge { flex: none; margin-left: .4rem; font-size: .7rem; line-height: 1; border-radius: var(--ci-radius); padding: .08rem .3rem; }
.ci-nav-badge.native { color: var(--ci-ok); }
.ci-nav-badge.legacy { color: var(--ci-ink-label); }

/* Modern navigation layout (default): grouped, icon-led, no port-status badges. */
.ci-icon { flex: none; display: inline-block; vertical-align: middle; }
.ci-nav.modern .ci-nav-link { justify-content: flex-start; gap: .6rem; border-radius: var(--ci-radius-sm); margin: .1rem .5rem; padding: .5rem .6rem; width: auto; }
.ci-nav.modern .active { background: rgba(255,255,255,.12); color: var(--ci-ink-on-brand); box-shadow: inset 3px 0 0 #4aa3e8; }
.ci-nav.modern .ci-nav-children { background: transparent; }
.ci-nav.modern .ci-nav-children .ci-nav-link { margin-left: 1.4rem; padding-left: .8rem; font-size: .86rem; border-left: 1px solid rgba(255,255,255,.12); border-radius: 0 var(--ci-radius-sm) var(--ci-radius-sm) 0; }
.ci-nav.modern .ci-nav-parent { justify-content: flex-start; }
.ci-nav.modern .ci-nav-parent .ci-nav-caret { margin-left: auto; }

/* ── Nested menu levels ──────────────────────────────────────────────────────────────────────
   Menus run four levels deep. Each level steps down in weight so depth is legible at a glance
   instead of every row looking equally important. */

/* A parent that is ALSO a page: the label navigates, the caret expands. Two targets, one row. */
.ci-nav.modern .ci-nav-parentrow { display: flex; align-items: stretch; }
.ci-nav.modern .ci-nav-parentrow .ci-nav-parentlink { flex: 1 1 auto; min-width: 0; margin-right: 0; }
.ci-nav.modern .ci-nav-caretbtn {
    flex: 0 0 auto; width: 26px; margin: .1rem .5rem .1rem 0; padding: 0;
    background: transparent; border: 0; cursor: pointer; color: rgba(255,255,255,.55);
    font-size: .7rem; line-height: 1; border-radius: var(--ci-radius-sm);
}
.ci-nav.modern .ci-nav-caretbtn:hover { background: rgba(255,255,255,.10); color: var(--ci-ink-on-brand); }

/* Level 3 and below: no icon (it is derived from the parent's route, so it just repeats down the
   branch), smaller and tighter, and stepped further right — the indent carries the nesting. */
.ci-nav.modern .ci-nav-deep {
    font-size: .82rem; padding-top: .34rem; padding-bottom: .34rem;
    color: rgba(255,255,255,.72);
}
.ci-nav.modern .ci-nav-children .ci-nav-children { margin-left: .55rem; }
.ci-nav.modern .ci-nav-children .ci-nav-children .ci-nav-link { font-size: .82rem; }
/* The guide rail fades as it goes deeper, so nesting reads without extra chrome. */
.ci-nav.modern .ci-nav-children .ci-nav-children .ci-nav-link,
.ci-nav.modern .ci-nav-children .ci-nav-children .ci-nav-parent { border-left-color: rgba(255,255,255,.07); }
/* The modern rail's section headers are `.ci-nav-link .ci-nav-parent` rows — see the note in CiModernNav. The old
   `.ci-nav-section-modern` / `-btn` / `-caret` rules are gone with them; `.ci-nav-section` above is still the
   CLASSIC nav's group label and is untouched.

   A group header is the same ROW as a link, and differs only by sitting back slightly: same size, same weight, same
   case, just a touch quieter until you point at it. Bold was tried first and read worse, not better — the groups
   then shouted over the plain destinations above them, which are the more important rows. Quieter is also the
   truthful signal: a header doesn't take you anywhere, it opens what does. Dimming the whole row (rather than
   picking a colour) keeps its icon and caret in step and survives a themed rail, where a hard-coded grey wouldn't.

   And the width is not decoration: `.ci-nav-link` above sets `width: auto`, which for the <a> links is harmless
   (they are block-level flex containers and fill the row anyway) but makes these <button> headers shrink to fit
   their own text — so `margin-left: auto` on the caret had no free space to push into and the caret sat against
   the label. Fill the row explicitly, minus the side margins each level carries. */
.ci-nav.modern > .ci-nav-parent { width: calc(100% - 1rem); margin-top: .12rem; opacity: .82; }
.ci-nav.modern .ci-nav-children > .ci-nav-parent { width: calc(100% - 1.9rem); opacity: .82; }
.ci-nav.modern .ci-nav-parent:hover,
.ci-nav.modern .ci-nav-parent:focus-visible,
.ci-nav.modern .ci-nav-parent[aria-expanded="true"] { opacity: 1; }
/* The drawn chevron on a top-level group. It carries the whole "this opens" message now that the row otherwise
   matches a link, so it is a real icon at a readable size, not the .6rem ▸ glyph the deeper rows still use. */
.ci-nav-chev { display: inline-flex; align-items: center; font-size: 0; opacity: .75; }
.ci-nav-parent:hover .ci-nav-chev, .ci-nav-parent[aria-expanded="true"] .ci-nav-chev { opacity: 1; }
/* Collapsed rail: icons centered, labels hidden, thin section separators. */
.ci-nav.modern.collapsed .ci-nav-link { justify-content: center; margin: .1rem .35rem; padding: .55rem 0; }
.ci-nav-sep { height: 1px; margin: .5rem .8rem; background: rgba(255,255,255,.12); }

/* Administration sits at the FOOT of the rail. The rail is a flex column and the pinned divider takes the leftover
   space, so on a short menu Administration drops to the bottom and on a long one the auto margin resolves to zero
   and it simply follows the last group (the rail scrolls). Every row is flex:none, or an overflowing menu would
   squash its rows instead of scrolling. */
.ci-nav.modern { display: flex; flex-direction: column; }
.ci-nav.modern > * { flex: 0 0 auto; }
.ci-nav-sep-pin { margin-top: auto; }

/* An AI destination (nav.json `ai: true`) — Create with VISDOM.
   Differentiated by SURFACE, not by text colour. Tinting the label was tried first and read as a broken link or a
   warning: every other row is white, so coloured words look like a fault rather than a feature. Instead the row
   carries a soft violet wash that fades out to the right (no hard box, so it doesn't look like a misplaced button)
   and the glyph alone takes the accent. Same 40px row as every other link, so nothing shifts. */
/* NO left bar, NO filled box and — since 2026-08-20 — NO resting background AT ALL. In this rail a fill means "you
   are here" (.ci-nav .active, a white wash + the accent bar), and the violet wash this row used to carry still read
   as a second active page: it faded out to the right, but the left half sat behind the label as a solid rounded
   block, which is the part the eye compares with the row above.
   The identity is carried by the glyph alone — violet body, white rim, slow shimmer (below) — which the active
   state can never be confused with.
   An "AI" pill used to sit at the end of the row as well. Removed 2026-09-14: the sparkle already says AI, on a row
   whose label is literally "Create with VISDOM", so the pill was saying the same thing a third time. If it ever
   comes back it belongs on a row whose label does NOT name the AI.
   A tint is still allowed on HOVER, where it is transient and therefore unambiguous. */
.ci-nav-link-ai { background: none; }
.ci-nav-link-ai .ci-nav-text { font-weight: 600; }
/* The registered mark on VISDOM. Drawn here, NOT written into the label, for two reasons:
   · on a seeded workspace the rail's labels come from t_HeaderMenuItems, not this file's nav.json, so a mark typed
     into nav.json would never appear on the row it was meant for; and
   · worse, NavTreeBuilder.IsAiByDefault matches the DB label against nav.json's BY TEXT to decide which row is the
     AI one — so adding a character to the nav.json label silently strips the whole VISDOM accent off the rail.
   Inside .ci-nav-text (not on the link) so it travels with the word and is clipped with it rather than floating at
   the row's right edge. .62em + a raised baseline is the smallest that still reads as a ® and not as a speck.

   TWO selectors, because the name appears in two places and missing one is invisible until someone works with the
   menu shut: the expanded row's label, and the collapsed rail's hover chip (`.ci-rail-label-name` — the collapsed
   ROW is a bare glyph with no label at all, so the chip is the only place the name is shown). A flyout item goes
   through the same .ci-nav-text span and is already covered. */
.ci-nav-link-ai .ci-nav-text::after,
.ci-rail-label-ai .ci-rail-label-name::after {
    content: "\00ae";
    font-size: .62em; font-weight: 700; vertical-align: .42em; letter-spacing: 0;
}
/* The glyph is FILLED violet with a WHITE OUTLINE, which is the only combination that reads at 20px on a blue rail.
   Every CiIcon is stroke-only (`fill="none"` on the <svg>), so as a plain tinted outline this sparkle was a few pale
   lilac hairlines and disappeared into the rail — it was the least prominent icon in the nav, not the most.
   Splitting the two jobs fixes it: the WHITE stroke supplies contrast against the blue (violet-on-blue has almost
   none, which is why the old lifted lilac still read as washed out), and the violet FILL supplies the identity.
   The reverse — violet outline, white body — was considered and loses: at this size the outline is most of the
   glyph, so it would read as a plain white icon with no VISDOM colour at all.
   A CSS `fill` beats the SVG's own fill="none" presentation attribute, so no markup change is needed. */
.ci-nav-link-ai svg {
    fill: var(--ci-ai-glyph-fill, #8b5cf6); stroke: var(--ci-ink-on-brand); color: var(--ci-ink-on-brand);
    /* Slightly thinner than the 1.9 every other glyph uses. 1.9 makes the white outline the whole star and leaves
       the violet a dot; 1.15 was a hairline that vanished against the rail. 1.65 keeps a rim you can see around a
       body that still reads purple. If this is retuned, judge it on the COLLAPSED rail — the glyph stands alone
       there, and this sparkle's arms are thin, so small changes here move the balance a lot. */
    stroke-width: 1.65;
    /* The "barely noticeable" part: the body drifts between two violets and the halo breathes with it, on a slow
       6s cycle. Slow and low-amplitude is the whole point — anything faster reads as a notification badge demanding
       a click, which is not what this row is. */
    animation: ci-ai-shimmer 6s ease-in-out infinite;
}
@keyframes ci-ai-shimmer {
    0%, 100% { fill: var(--ci-ai-glyph-fill, #8b5cf6); filter: drop-shadow(0 0 1px rgba(167, 139, 250, .45)); }
    50%      { fill: #a78bfa;                          filter: drop-shadow(0 0 3px rgba(196, 181, 253, .75)); }
}
/* The glyph still reads as the AI one without moving — the violet body and white rim were always doing that work;
   the shimmer only adds a little life on top of it. So this drops to the static state, not to a fallback look. */
@media (prefers-reduced-motion: reduce) {
    .ci-nav-link-ai svg { animation: none; filter: drop-shadow(0 0 2px rgba(167, 139, 250, .55)); }
}
/* Hover is a FLAT violet across the whole row — the violet is what marks this row as the AI one, and covering the row
   evenly is what keeps it reading as a hover (every other row hovers the same shape in white) rather than as the
   left-weighted block the old resting wash was mistaken for. */
.ci-nav-link-ai:hover { background: rgba(139, 124, 246, .26); }
/* Hover brightens the body rather than washing the glyph out to white — going fully white on hover would make it
   momentarily indistinguishable from the active state. The shimmer stops, because on hover the pointer is already
   saying "this one" and a moving target under the cursor is just noise. */
.ci-nav-link-ai:hover svg { animation: none; fill: #a78bfa; filter: drop-shadow(0 0 3px rgba(196, 181, 253, .8)); }
/* Active wins outright: "where am I" must never compete with "this is the AI one", so the row reverts to the app's
   own active treatment (white text + the accent bar) and drops the wash. The glyph goes solid white for the same
   reason — and losing the violet costs nothing here, because the page you are on is obviously the VISDOM one. */
.ci-nav .active.ci-nav-link-ai { background: rgba(255,255,255,.10); box-shadow: inset 3px 0 0 var(--ci-accent); }
.ci-nav .active.ci-nav-link-ai svg { animation: none; filter: none; fill: var(--ci-ink-on-brand); stroke: var(--ci-ink-on-brand); color: var(--ci-ink-on-brand); }

/* Collapsed rail: one icon per section, and a hover flyout listing that section's links. The flyout is
   position:fixed so the nav's own vertical scroll (overflow-y:auto) can't clip it. */
.ci-rail-group { position: relative; }
.ci-rail-trigger { cursor: pointer; }
.ci-rail-trigger.on { background: rgba(255,255,255,.12); color: var(--ci-sidebar-fg-hover); }
/* A <button> defaults to shrink-to-fit, so the section triggers would be narrower + left-aligned vs the <a> nav
   links. Give the group the same inset the links have and stretch the button to fill it, so the icon lands dead
   center like the top items and gets the same full-width hover/active pill. */
.ci-nav.modern.collapsed .ci-rail-group { margin: .1rem .35rem; }
.ci-nav.modern.collapsed .ci-rail-trigger { width: 100%; margin: 0; }
/* WHERE IT OPENS. CiModernNav hands in --ci-fly-y: the pointer's Y, already divided by the interface zoom (a `top`
   on a fixed descendant of a zoomed root paints at top × zoom, while a mouse event's clientY arrives in plain
   viewport px — see the note at the call site).
   Keeping it on screen is done with the panel's OWN height rather than a guess at it. The old rule clamped `top` to
   `100vh - 320px`, which is wrong twice over: 320px is not any particular flyout's height, and a vh bound is itself
   scaled by the zoom. A SHORT flyout — Library, three links — was therefore shoved up to 320px above the bottom and
   no longer reached the icon that opened it, so the pointer was left outside it and the trip across dismissed it.
   `translateY` with a PERCENTAGE resolves against the element's own height, so `min(0px, …- 100%)` lifts the panel by
   exactly the amount it overhangs and by nothing at all when it fits. Measured at 100/120/150/80%: the panel's top
   sits 24px above the pointer at every zoom, on every row. */
.ci-nav-flyout {
    position: fixed; left: calc(var(--ci-sidebar-w-collapsed) + 2px); z-index: var(--ci-z-navflyout);
    top: max(8px, var(--ci-fly-y, 8px));
    transform: translateY(min(0px, calc(var(--ci-vh) - 8px - var(--ci-fly-y, 8px) - 100%)));
    /* --ci-vh, not vh, for the same reason: a plain 84vh is multiplied by the zoom when it paints, which let the
       panel run past the bottom of the window at 125% and up. */
    min-width: 208px; max-width: 268px; max-height: calc(var(--ci-vh) * 0.84); overflow-y: auto;
    /* Same ink as the bottom of the rail's gradient, so a theme change carries into the flyout. */
    background: var(--ci-navy-dark); border: 1px solid rgba(255,255,255,.14); border-radius: var(--ci-radius);
    box-shadow: 0 12px 32px rgba(8,14,30,.5); padding: .35rem;
}
/* Invisible bridge so moving the cursor across the small rail↔flyout gap keeps the hover group active. */
.ci-nav-flyout::before { content: ""; position: absolute; left: -14px; top: 0; width: 14px; height: 100%; }
.ci-nav-flyout-head { text-transform: uppercase; font-size: var(--ci-fs-label); font-weight: var(--ci-fw-label); letter-spacing: var(--ci-ls-label);
    color: var(--ci-ink-label); padding: .45rem .65rem .35rem; }
.ci-nav-flyout-items { display: flex; flex-direction: column; gap: 1px; }
.ci-nav-flyout-item { display: flex; align-items: center; gap: .55rem; padding: .5rem .65rem; border-radius: var(--ci-radius-sm);
    color: var(--ci-sidebar-fg); text-decoration: none; font-size: .9rem; white-space: nowrap; }
.ci-nav-flyout-item:hover { background: rgba(255,255,255,.10); color: var(--ci-sidebar-fg-hover); }
.ci-nav-flyout-item.active { background: rgba(255,255,255,.14); color: var(--ci-ink-on-brand); box-shadow: inset 3px 0 0 var(--ci-accent); }
.ci-nav-flyout-item .ci-icon { flex: none; opacity: .85; }
/* A group inside the flyout: its name, then its items indented behind a guide rail — the same shape (and the same
   guide) the expanded menu uses for a nested group, so the two views of one menu read as the same tree. */
.ci-nav-flyout-sub { font-size: var(--ci-fs-label); font-weight: var(--ci-fw-label); letter-spacing: var(--ci-ls-label);
    color: var(--ci-ink-label); padding: .45rem .65rem .25rem; }
.ci-nav-flyout-nest { display: flex; flex-direction: column; gap: 1px;
    margin-left: .7rem; padding-left: .5rem; border-left: 1px solid rgba(255,255,255,.14); }

/* A rail item with no section under it gets the same flyout, carrying just its NAME (and what it's for, when the
   menu says). The browser's own tooltip can't do this job: it waits about a second before appearing, which on a rail
   of unlabelled glyphs means hunting — and it can't show two lines. Same surface as the section flyout, so the rail
   behaves one way whichever kind of item you're on. */
.ci-rail-label { padding: .45rem .7rem; min-width: 0; max-width: 260px; }
.ci-rail-label-name { color: var(--ci-sidebar-fg-hover); font-size: .88rem; font-weight: 600; white-space: nowrap; }
.ci-rail-label-hint { color: var(--ci-ink-label); font-size: .76rem; line-height: 1.35; margin-top: .15rem; }

/* Pinned sidebar footer: the link to the Main Menu designer (admins only). */
.ci-nav-foot { border-top: 1px solid rgba(255,255,255,.10); padding: .55rem .6rem; }
.ci-nav-editlink { display: flex; align-items: center; gap: .45rem; padding: .35rem .55rem; border-radius: var(--ci-radius-sm);
    color: rgba(255,255,255,.5); text-decoration: none; font-size: .76rem; font-weight: 600;
    transition: background var(--ci-transition), color var(--ci-transition); }
.ci-nav-editlink:hover { background: rgba(255,255,255,.12); color: var(--ci-ink-on-brand); }

/* A menu item whose target is reachable from neither this app nor a configured classic site. Disabled with a
   reason tooltip rather than hidden, so an admin can see WHY their authored item isn't clickable (rule 9). */
.ci-nav-link-disabled { opacity: .42; cursor: not-allowed; }
.ci-nav-link-disabled:hover { background: transparent; }
/* A greyed item that still explains itself: same muted look (the menu stays an honest map of what this app has),
   but it is a real link to the page that says what replaced it — so the cursor must not read as "dead". */
.ci-nav-link-signpost { cursor: pointer; }
.ci-nav-link-signpost:hover { opacity: .68; }
.ci-nav-empty { padding: .8rem .9rem; font-size: .78rem; font-style: italic; color: rgba(255,255,255,.45); }

/* ── Where you are ────────────────────────────────────────────────────────────────────────────────────────────
   `.active` is the row you are ON. `.is-trail` is every group that CONTAINS it, which is the only thing that can
   answer "where am I" when the page is three levels down or its section renders as a single link (Administration).
   Deliberately quieter than .active — same accent, half strength — so the current row still wins. */
.ci-nav.modern .ci-nav-link.is-trail,
.ci-nav.modern .ci-nav-parent.is-trail {
    opacity: 1; color: var(--ci-sidebar-fg-hover); box-shadow: inset 3px 0 0 rgba(74, 163, 232, .5);
}

/* Main column */
.ci-main { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.ci-topbar {
    height: var(--ci-topbar-h); background: var(--ci-topbar-bg);
    border-bottom: 1px solid var(--ci-topbar-border);
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 1.25rem; flex-shrink: 0;
    box-shadow: var(--ci-shadow-sm);
    /* Own stacking context ABOVE page content so header dropdowns (account/apps/bell) paint over
       positioned page elements — e.g. the dashboard hero's date, which also sits at the top-right and
       would otherwise tie on z-index and win on DOM order, bleeding through the open panel. */
    position: relative; z-index: 20;
    gap: .5rem;
}
/* Every top-bar control shares one recipe: --ci-ctl-h tall, --ci-radius corners, --ci-ctl-border border,
   --ci-ctl-bg resting fill — so the row reads as one family.
   ★ The radius was --ci-radius-pill, i.e. fully round at 36px tall, which made the top bar the one part of the app
   that didn't look like the app (owner, 2026-09-06: "everything else is a lil more square"). The tell is the header's
   + Add sitting directly above a list page's own + Add: the same blue button, the same words, two different shapes.
   ★ The radius is --ci-radius-lg, and that is a DELIBERATE break of U12's "pick the step by the size of the
   element" (owner call, 2026-09-06 — U10 says a difference has to be justified out loud, so: this is it). By size
   these are controls and would take 4px; tried, and the owner read it as "a little too square". The bar is the
   app's FRAME, not something inside a form, and the thing it has to agree with is the surface immediately under it
   — the Home hero, which is --ci-radius-lg. Matching it makes the header and the page read as one sheet. Do not
   "correct" this back to the control step. Pill stays where the shape IS the identity — a count badge, a status
   chip, the avatar disc. */
/* Header search — a BUTTON, not an input: it opens the search dialog (Components/Shared/SearchDialog.razor), which
   owns the typing, the filters and the live results. 240px = 75% of the 320px box it replaced. */
.ci-searchbtn {
    display: inline-flex; align-items: center; gap: .5rem; width: 240px; max-width: 30vw; min-width: 0;
    height: var(--ci-ctl-h); padding: 0 .6rem 0 1rem; cursor: pointer; text-align: left;
    border: 1px solid var(--ci-ctl-border); border-radius: var(--ci-radius-lg); font-size: .9rem; background: var(--ci-ctl-bg);
    color: var(--ci-ink-support);
    transition: border-color var(--ci-transition), box-shadow var(--ci-transition), background var(--ci-transition),
                color var(--ci-transition);
}
.ci-searchbtn:hover {
    background: var(--ci-surface); border-color: var(--ci-accent); color: var(--ci-brand-ink); box-shadow: var(--ci-shadow-sm);
}
.ci-searchbtn:focus-visible { outline: none; border-color: var(--ci-accent); background: var(--ci-surface); box-shadow: var(--ci-ring); }
.ci-searchbtn-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The shortcut hint doubles as the affordance that this is a control, not a text field. */
.ci-searchbtn-kbd {
    flex: 0 0 auto; border: 1px solid var(--ci-ctl-border); border-bottom-width: 2px; border-radius: var(--ci-radius-sm);
    background: var(--ci-surface); color: var(--ci-ink-faint); font-family: inherit; font-size: .7rem; line-height: 1.3;
    padding: .05rem .32rem;
}
.ci-searchbtn:hover .ci-searchbtn-kbd { border-color: var(--ci-line-strong); color: var(--ci-accent-ink); }
/* The search absorbs the horizontal squeeze (it can shrink); the action buttons on the right and the Drag & Drop
   pill never shrink/scrunch — so a wider sidebar just makes the search smaller, not the buttons. (Left keeps the
   default flex so space-between still centers the Drag & Drop pill.) */
.ci-topbar-left { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.ci-topbar-right { display: flex; align-items: center; gap: .7rem; flex-shrink: 0; }
/* The utility-glyph cluster (see MainLayout). A tight gap on purpose: these are ghost buttons, so nothing but
   proximity says they belong together, and each one's 36px box already carries its own breathing room — the
   glyphs inside end up about .55rem apart, which looks like a toolbar rather than a tight row. */
.ci-hglyphs { display: flex; align-items: center; gap: .1rem; flex-shrink: 0; }
/* The customer's own logo, first thing in the header. Constrained on BOTH axes and never allowed to shrink the
   search: whatever anyone uploads — a tall square badge, a very wide wordmark, a 3000px original — has to sit in
   this box without changing the height of the header or the position of anything beside it. max-width is in rem so
   a wide logo gives up room, not legibility, at the smaller responsive tiers. */
.ci-clientlogo { height: 26px; width: auto; max-width: 13rem; object-fit: contain; object-position: left center; flex: none; }
/* 13rem is not arbitrary: a wordmark-shaped logo is around 7:1, and 7 × 26px ≈ 182px, so the cap lets a normal
   wide logo render at its full height instead of being letterboxed down to unreadable. Anything wider than the
   cap loses height rather than being cropped (object-fit), which is why the narrow tier only trims it a little. */
@media (max-width: 1100px) { .ci-clientlogo { max-width: 9.5rem; } }
/* Below the drawer breakpoint the header is down to the essentials (burger + search + actions); branding is the
   first thing to go, because a logo squeezing the search box is a worse trade than no logo. */
@media (max-width: 700px) { .ci-clientlogo, .ci-clientlogo + .ci-topbar-div { display: none; } }
/* Translating — the whole-UI sweep is running. Hidden unless ci-translate.js has put .ci-translating on <html>,
   which it does only while requests are in flight. Bottom-LEFT so it can't sit under the VISDOM launcher, and
   pointer-events off so it never blocks what is behind it. (The language control itself is in the account menu;
   this is the only permanent chrome the feature keeps.) */
.ci-xlate-busy {
    display: none; position: fixed; left: 1rem; bottom: 1rem; z-index: var(--ci-z-toast, 1300);
    align-items: center; gap: .45rem; padding: .35rem .7rem; border-radius: var(--ci-radius-pill);
    background: var(--ci-navy); color: var(--ci-ink-on-brand); font-size: var(--ci-fs-meta, .78rem); font-weight: 600;
    box-shadow: 0 6px 18px rgba(16, 24, 40, .28); pointer-events: none;
}
.ci-translating .ci-xlate-busy { display: inline-flex; }
.ci-xlate-spin {
    width: 12px; height: 12px; border-radius: var(--ci-radius-pill); border: 2px solid rgba(255, 255, 255, .35);
    border-top-color: var(--ci-surface); animation: ci-xlate-spin .7s linear infinite;
}
@keyframes ci-xlate-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ci-xlate-spin { animation: none; } }
.ci-user { color: var(--ci-ink-body); font-weight: 600; font-size: .92rem; }
.ci-account-link { color: var(--ci-ink-2); text-decoration: none; font-size: .85rem; }
.ci-account-link:hover { color: var(--ci-accent-ink); text-decoration: underline; }
.ci-logout {
    color: var(--ci-accent-ink); text-decoration: none; font-size: .9rem;
    border: 1px solid var(--ci-accent); padding: .35rem .8rem; border-radius: var(--ci-radius-sm);
}
.ci-logout:hover { background: var(--ci-accent); color: var(--ci-ink-on-brand); }

/* ───────────────────────────── Top-bar menus (apps launcher / + New / account) ─────────────────────────────
   Shared styling for the header dropdowns. Each menu is a button + a click-away backdrop + a floating panel
   (same interaction the notification bell uses). Dark-on-white to match the light top bar. */
.ci-hmenu { position: relative; display: inline-flex; align-items: center; }
.ci-hmenu-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
    height: var(--ci-ctl-h); padding: 0 .85rem; border-radius: var(--ci-radius-lg);
    background: var(--ci-ctl-bg); border: 1px solid var(--ci-ctl-border); color: var(--ci-brand-ink);
    cursor: pointer; font-size: .88rem; font-weight: 600;
    transition: background var(--ci-transition), color var(--ci-transition), border-color var(--ci-transition);
}
.ci-hmenu-btn:hover { background: var(--ci-navy); border-color: var(--ci-line-emphasis); color: var(--ci-ink-on-brand); }
/* The header's ONE primary action (+ Add). A modifier rather than a change to .ci-hmenu-btn itself, because that
   base class is also the apps launcher's — a shared class that only has one consumer today still shouldn't
   silently become "the blue one". Accent, not navy: navy is the rail and the grid header (chrome you look PAST),
   while accent is what the app uses for the thing you act on, and every page's own primary button is already
   this colour — so the header's create action now matches the create action on the page below it. */
.ci-hmenu-btn-primary { background: var(--ci-accent); border-color: var(--ci-accent); color: var(--ci-ink-on-brand); }
.ci-hmenu-btn-primary:hover { background: var(--ci-accent-dark); border-color: var(--ci-accent-dark); color: var(--ci-ink-on-brand); }
/* The apps launcher joins the utility-glyph tier — see --ci-ctl-ghost-* in :root for why. */
.ci-hmenu-icon-btn { width: var(--ci-ctl-h); padding: 0; background: var(--ci-ctl-ghost-bg);
                     border-color: var(--ci-ctl-ghost-bd); color: var(--ci-ctl-ghost-fg); }
.ci-hmenu-icon-btn:hover { background: var(--ci-ctl-ghost-bg-hover); border-color: var(--ci-ctl-ghost-bd);
                           color: var(--ci-brand-ink); }
.ci-hmenu-icon-btn:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: -2px; }
.ci-hmenu-plus { font-size: 1.05rem; line-height: 1; font-weight: 700; }

.ci-hmenu-backdrop { position: fixed; inset: 0; z-index: 1000; background: transparent; }
.ci-hmenu-panel {
    position: absolute; top: calc(100% + 8px); right: 0; background: var(--ci-surface);
    border: 1px solid var(--ci-line); border-radius: var(--ci-radius); box-shadow: var(--ci-shadow-lg);
    z-index: 1001; overflow: hidden; min-width: 220px;
}
/* Menus that live on the LEFT side of the top bar (e.g. the Go-to launcher) anchor their panel left. */
.ci-hmenu-left .ci-hmenu-panel { left: 0; right: auto; }
.ci-hmenu-panel-narrow { width: 244px; }
.ci-hmenu-panel-apps { width: 372px; max-width: 92vw; }
.ci-hmenu-head {
    padding: 10px 14px; background: var(--ci-surface-sunk); border-bottom: 1px solid var(--ci-line);
    font-weight: 600; color: var(--ci-brand-ink); font-size: .85rem;
}
.ci-hmenu-item {
    display: flex; align-items: center; gap: .6rem; padding: .55rem .9rem;
    color: var(--ci-ink-body); text-decoration: none; font-size: .88rem;
    transition: background var(--ci-transition), color var(--ci-transition);
}

/* A count riding on a menu entry (My Drafts). Pushed to the row's far edge so the label column stays aligned;
   quiet by design — it says "you have some", it is not an alert. */
.ci-hmenu-count {
    margin-left: auto; padding: 0 .45rem; border-radius: var(--ci-radius-pill);
    background: var(--ci-hover-strong); color: var(--ci-brand-ink);
    font-size: .72rem; font-weight: 700; line-height: 1.3rem; min-width: 1.3rem; text-align: center;
    font-variant-numeric: tabular-nums;
}
.ci-hmenu-item:hover { background: color-mix(in srgb, var(--ci-accent) 6%, var(--ci-surface)); color: var(--ci-accent-ink); }
.ci-hmenu-item-icon { display: inline-flex; color: var(--ci-slate); }
.ci-hmenu-item:hover .ci-hmenu-item-icon { color: var(--ci-accent-ink); }
/* An entry that exists but isn't available — greyed, with the reason in its tooltip, rather than removed (rule 10:
   disable with a reason, never hide). Rendered as a span, so there is nothing to click and nowhere to go. */
.ci-hmenu-item-off { color: var(--ci-ink-faint); cursor: default; }
.ci-hmenu-item-off:hover { background: transparent; color: var(--ci-ink-faint); }
.ci-hmenu-item-off .ci-hmenu-item-icon { color: var(--ci-ink-disabled); }
.ci-hmenu-item-off .ci-hmenu-item-why { margin-left: auto; font-size: .72rem; color: var(--ci-ink-faint); white-space: nowrap; }
.ci-hmenu-item-danger { color: var(--ci-danger); }
.ci-hmenu-item-danger:hover { background: var(--ci-danger-surface); color: var(--ci-danger); }
.ci-hmenu-item-danger .ci-hmenu-item-icon { color: var(--ci-danger); }
.ci-hmenu-empty { padding: 16px 14px; color: var(--ci-ink-quiet); font-size: .84rem; text-align: center; }
.ci-hmenu-sep { height: 1px; background: var(--ci-surface-sunk); margin: .3rem 0; }

/* Apps launcher grid */
.ci-apps { padding: .4rem .6rem .6rem; max-height: 62vh; overflow-y: auto; }
.ci-apps-group-label {
    text-transform: uppercase; color: var(--ci-ink-quiet); font-size: var(--ci-fs-label); letter-spacing: var(--ci-ls-label);
    padding: .6rem .4rem .3rem; font-weight: 700;
}
.ci-apps-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .25rem; }
.ci-apps-tile {
    display: flex; flex-direction: column; align-items: center; gap: .4rem; text-align: center;
    padding: .7rem .3rem; border-radius: var(--ci-radius); color: var(--ci-ink-body); text-decoration: none;
    font-size: .72rem; line-height: 1.15; transition: background var(--ci-transition), color var(--ci-transition);
}
.ci-apps-tile:hover { background: color-mix(in srgb, var(--ci-accent) 8%, var(--ci-surface)); color: var(--ci-accent-ink); }
.ci-apps-tile .ci-icon { color: var(--ci-brand-ink); }
.ci-apps-tile:hover .ci-icon { color: var(--ci-accent-ink); }

/* Account menu */
.ci-acct-btn {
    display: inline-flex; align-items: center; gap: .5rem; height: var(--ci-ctl-h);
    background: var(--ci-ctl-bg); border: 1px solid var(--ci-ctl-border); border-radius: var(--ci-radius-lg);
    /* The BUTTON squares up with the rest of the bar; the avatar inside it stays a disc (below) — a portrait is
       round everywhere, and that is the "the shape IS the identity" case the pill token exists for. */
    padding: 0 .6rem 0 4px; cursor: pointer; color: var(--ci-brand-ink);
    transition: background var(--ci-transition), border-color var(--ci-transition), color var(--ci-transition);
}
.ci-acct-btn:hover { background: var(--ci-navy); border-color: var(--ci-line-emphasis); color: var(--ci-ink-on-brand); }
.ci-acct-btn:hover .ci-acct-avatar { background: var(--ci-surface); color: var(--ci-brand-ink); }
.ci-acct-btn:hover .ci-acct-caret { color: var(--ci-ink-disabled); }
/* Sized so the disc clears the button's edge on all four sides: the button is --ci-ctl-h (36px) tall, so 27px
   leaves ~4px above and below, and the 4px left padding above matches it. A portrait that touches the pill's
   border reads as cropped — the ring of button colour around it is what makes it read as a portrait. It was 28px
   in a 36px button (2px of clearance, which read as flush); 26 was tried and looked shrunken. */
.ci-acct-avatar {
    width: 27px; height: 27px; border-radius: 50%; background: var(--ci-navy); color: var(--ci-ink-on-brand);
    display: inline-flex; align-items: center; justify-content: center; font-size: .76rem; font-weight: 700; flex: none;
    transition: background var(--ci-transition), color var(--ci-transition);
}
.ci-acct-avatar.lg { width: 38px; height: 38px; font-size: .9rem; }
.ci-acct-avatar.xl { width: 96px; height: 96px; font-size: 2rem; }
img.ci-acct-avatar { object-fit: cover; background: var(--ci-hover); }

/* Profile-picture dialog (opened from the account menu). The panel, header, scrim, layering and button row are
   CiDialog's — what's left here is only the BODY: the round preview and the file picker. The dialog used to
   re-declare .ci-btn/.ci-btn-secondary/.ci-btn-danger under its own class because those bases were once page-scoped;
   they are global (see the button tiers below), so the copies are gone and this dialog's buttons now match the app. */
.ci-photo-body { display: flex; flex-direction: column; align-items: center; gap: .8rem; }
.ci-photo-preview img {
    width: 96px; height: 96px; border-radius: 50%; object-fit: cover;
    border: 3px solid var(--ci-line); background: var(--ci-hover);
}
.ci-photo-pick input[type="file"] { display: none; }
.ci-photo-pick-btn { cursor: pointer; }
.ci-photo-hint { color: var(--ci-ink-quiet); font-size: .78rem; text-align: center; line-height: 1.35; }
.ci-photo-error { color: var(--ci-danger); font-size: .82rem; text-align: center; }

/* Button-tag variant of the shared header-menu item (same look as the <a> items). */
.ci-hmenu-item-btn { width: 100%; background: transparent; border: 0; cursor: pointer; text-align: left; font: inherit; font-size: .88rem; }

/* VISDOM+ toggle row in the account menu (port of the legacy UserMenu iOS-style slider). */
.ci-visdom-toggle { display: flex; align-items: center; justify-content: space-between; gap: .6rem; padding: .3rem .9rem .2rem;}
.ci-visdom-toggle-label { display: inline-flex; align-items: center; gap: .35rem; font-size: .85rem; font-weight: 600; color: var(--ci-ink-body); }
.ci-visdom-help {
    display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px;
    border-radius: 50%; background: var(--ci-hover); color: var(--ci-ink-2); font-size: .66rem; font-weight: 700; cursor: help;
}
/* Interface scale row (Account menu) — same shape as the VISDOM toggle row above it: label left, control right. */
.ci-scale-row { display: flex; align-items: center; justify-content: space-between; gap: .6rem; padding: 0 .9rem; }
.ci-scale-label { display: inline-flex; align-items: center; gap: .35rem; font-size: .85rem; font-weight: 600; color: var(--ci-ink-body); }
/* A single segmented control rather than three loose buttons — the value belongs between its own − and +. */
.ci-scale-ctl { display: inline-flex; align-items: center; flex: none; border: 1px solid var(--ci-line); border-radius: var(--ci-radius-pill);
                background: var(--ci-surface-2); overflow: hidden; }
.ci-scale-btn, .ci-scale-val { border: 0; background: none; cursor: pointer; color: var(--ci-ink-body); font: inherit; }
.ci-scale-btn { width: 26px; height: 24px; font-size: .95rem; line-height: 1; color: var(--ci-brand-ink); }
.ci-scale-btn:hover:not(:disabled) { background: var(--ci-hover); }
.ci-scale-btn:disabled { color: var(--ci-ink-disabled); cursor: default; }
/* The value doubles as "reset to 100%", so it reads as pressable without shouting. */
.ci-scale-val { min-width: 3.1rem; padding: 0 .1rem; font-size: .78rem; font-weight: 700; color: var(--ci-brand-ink);
                border-left: 1px solid var(--ci-line); border-right: 1px solid var(--ci-line); height: 24px; }
.ci-scale-val:hover { background: var(--ci-hover); }

.ci-switch { position: relative; display: inline-block; width: 38px; height: 20px; flex: none; cursor: pointer; }
.ci-switch input { opacity: 0; width: 0; height: 0; }
.ci-switch-slider {
    position: absolute; inset: 0; border-radius: var(--ci-radius-pill); background: var(--ci-line-strong);
    transition: background var(--ci-transition);
}
.ci-switch-slider::before {
    content: ""; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; border-radius: 50%;
    background: var(--ci-surface); box-shadow: 0 1px 2px rgba(16,24,40,.25); transition: transform var(--ci-transition);
}
.ci-switch input:checked + .ci-switch-slider { background: var(--ci-accent); }
.ci-switch input:checked + .ci-switch-slider::before { transform: translateX(18px); }
.ci-switch input:disabled + .ci-switch-slider { opacity: .55; }
/* A disabled toggle kept the pointer from .ci-switch, so it still said "click me" while doing nothing (pointer
   audit, 2026-09-06). The cursor lives on the LABEL, which is what the mouse is actually over. */
.ci-switch:has(input:disabled) { cursor: not-allowed; }
/* A switch with its state beside it - "[switch] On". The state word is what a screen reader and a glance both need;
   a sentence restating the setting's label is not (CS-11). MFA and SSO carried the same row privately. */
.ci-switch-line { display: inline-flex; align-items: center; gap: .55rem; }
.ci-switch-state { font-size: .84rem; font-weight: 600; color: var(--ci-ink-support); }

/* ── A RADIO CHOICE WITH ITS CONSEQUENCE UNDER IT ────────────────────────────────────────────────────────────
   For a setting whose options each need one sentence to choose between (how much an agent may do on its own, what
   happens to a duplicate). The whole box is the hit target; the chosen one takes the accent edge. Lifted from the
   Intake Agent's settings so the Agent Studio drawer, which edits the same switch, looks the same. Layout of the set
   (one column, three across) stays with the page: wrap them in `.ci-choices` or a grid of your own. */
.ci-choices { display: grid; gap: .45rem; }
.ci-choice { display: grid; grid-template-columns: auto 1fr; gap: 0 .6rem; align-items: start; padding: .55rem .7rem;
    border: 1px solid var(--ci-line); border-radius: var(--ci-radius-sm); background: var(--ci-surface); cursor: pointer;
    transition: border-color var(--ci-transition), background var(--ci-transition); }
.ci-choice:hover { border-color: var(--ci-line-strong); }
.ci-choice.is-on { border-color: var(--ci-accent); background: var(--ci-surface-2); }
.ci-choice input { margin: .25rem 0 0; accent-color: var(--ci-accent); }
.ci-choice:has(input:disabled) { cursor: not-allowed; opacity: .6; }
.ci-choice-t { display: block; font-size: .86rem; font-weight: 700; color: var(--ci-ink-body); }
.ci-choice-d { display: block; font-size: .77rem; color: var(--ci-ink-support); }

/* ── A COUNT TILE ─────────────────────────────────────────────────────────────────────────────────────────────
   The number and what it counts, the whole tile being the way in: Home's "needs you" strip, and Agent Studio's
   summary. Quiet at zero and coloured only when there is something to do, so the eye lands on the count that matters
   instead of four identical boxes. Lifted verbatim from the Summary widget's own style block (a component's block
   only exists while that component is on the page); the grid that lays the tiles out stays with each page.
   `.is-static` is a tile that is not a way in, so it gives no hover answer (U14: a cursor must not lie). */
.ci-needs-tile { display: flex; flex-direction: column; gap: .1rem; padding: .6rem .85rem; background: var(--ci-surface);
                 border: 1px solid var(--ci-line); border-left: 3px solid var(--ci-line-strong); border-radius: var(--ci-radius);
                 text-decoration: none; color: var(--ci-brand-ink);
                 transition: border-color var(--ci-transition), box-shadow var(--ci-transition); }
.ci-needs-tile:hover { border-color: var(--ci-line-strong); box-shadow: 0 2px 8px rgba(20, 45, 80, .07); }
.ci-needs-tile.is-static:hover { border-color: var(--ci-line); border-left-color: var(--ci-line-strong); box-shadow: none; }
/* A tile that acts on the page (filters a list on it) is a button, so it needs the button reset to sit in the same
   grid as the links: a button brings its own font, centring and 2px border. */
.ci-needs-btn { align-items: flex-start; text-align: left; font: inherit; cursor: pointer; }
/* The pressed tile. Ring rather than a fill: the tile already uses its background for "urgent". */
.ci-needs-tile.on { border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
.ci-needs-n { font-size: 1.5rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; color: var(--ci-ink-faint); }
.ci-needs-l { display: inline-flex; align-items: center; gap: .3rem; font-size: .76rem; font-weight: 600;
              color: var(--ci-ink-support); white-space: nowrap; }
/* Something to do: the number takes the accent. Alarm colour is for overdue only. */
.ci-needs-tile.warn { border-left-color: var(--ci-accent); }
.ci-needs-tile.warn .ci-needs-n { color: var(--ci-accent-ink); }
.ci-needs-tile.urgent { border-left-color: var(--ci-danger); background: var(--ci-surface-2); }
.ci-needs-tile.urgent .ci-needs-n { color: var(--ci-danger); }
/* A figure that is information rather than a to-do (a total, a rate) reads in the body ink, not the faint zero ink. */
.ci-needs-tile.is-fact .ci-needs-n { color: var(--ci-brand-ink); }

/* ───────────────────────────── Header drag-and-drop intake ─────────────────────────────
   The pill is the affordance; the real drop target is the whole page (document-level listeners in
   App.razor ciDrop show the overlay while files are dragged). */
.ci-drop-host { display: inline-flex; align-items: center; min-width: 0; flex-shrink: 0; }
.ci-drop-pill-label { white-space: nowrap; }
/* ── AN OPEN SLOT IN THE BAR, not an object sitting on it ──────────────────────────────────────────────
   The header has ONE recipe — --ci-ctl-h tall, 999px radius, --ci-ctl-border — and every control follows it: the
   search, + New, the icon buttons, the account chip. This used to break all three (12px corners, its own border
   colour, a blue gradient) AND carry a second rounded rectangle inside itself: a dashed liner inset 3px at a 9px
   radius inside a 12px-radius box. Two concentric rounded rects whose corners don't agree, with a pale channel
   between them, is exactly what reads as pasted on — the eye sees a sticker with a border round it.

   So: one border, and the bar's own recipe. Same height, same radius, same border TOKEN as its neighbours —
   but DASHED and unfilled, which is the one treatment that means "an empty place things go" rather than "a button".
   Unfilled means its centre is the header's own white: it is a slot cut in the bar, not a tile laid on it.

   The states then have somewhere to go, which the old well didn't: hover and drag-in-flight turn the dash solid and
   wash the inside with accent — the slot MATERIALISING to receive. (The real target is the whole page; this is the
   affordance, so at rest it should be the quietest thing in the row.) */
.ci-drop-pill {
    position: relative;
    display: inline-flex; align-items: center; gap: .45rem; height: var(--ci-ctl-h);
    padding: 0 .95rem; border-radius: var(--ci-radius-lg);
    border: 1px dashed var(--ci-ctl-border);
    background: transparent;
    /* The same quiet grey the header search button uses for its resting label, so the two read as one voice. */
    color: var(--ci-ink-support); font-size: .85rem; font-weight: 600; cursor: pointer;
    transition: background var(--ci-transition), border-color var(--ci-transition), color var(--ci-transition),
                box-shadow var(--ci-transition);
}
.ci-drop-pill .ci-icon { color: var(--ci-ink-faint); transition: color var(--ci-transition), transform var(--ci-transition); }
/* Hover and drag-in-flight are the SAME state — in both, the thing about to receive a file lights up. Solid border,
   faint accent wash: the outline fills in rather than the control changing shape or lifting off the bar. */
.ci-drop-pill:hover, .ci-drop-pill.is-armed {
    border-style: solid; border-color: var(--ci-accent); color: var(--ci-brand-ink);
    background: var(--ci-surface-sunk); background: color-mix(in srgb, var(--ci-accent) 7%, var(--ci-surface));
}
.ci-drop-pill:hover .ci-icon, .ci-drop-pill.is-armed .ci-icon { color: var(--ci-accent-ink); }
.ci-drop-pill:hover .ci-icon { transform: translateY(-1px); }
/* A drag IS in flight and this is what it is aimed at — the one moment the control is allowed to shout. */
.ci-drop-pill.is-armed { box-shadow: var(--ci-ring); }
.ci-drop-pill:active {
    background: var(--ci-hover); background: color-mix(in srgb, var(--ci-accent) 13%, var(--ci-surface)); }
/* :has(:focus-visible), not :focus-visible: the pill is a LABEL and never takes focus itself — Tab lands on the
   visually-hidden file input inside it, so the old selector never matched and a keyboard user got no ring (bug 46237). */
.ci-drop-pill:focus-visible, .ci-drop-pill:has(:focus-visible) {
    outline: none; border-style: solid; border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
/* The AI hint, and nothing more: a small steady glyph. It used to breathe on a 3.6s loop, which made the one control
   meant to recede into the chrome the only moving thing in a static bar. It brightens on hover like everything else. */
.ci-drop-pill-spark {
    display: inline-flex; align-items: center; justify-content: center; width: 14px; flex-shrink: 0;
    color: var(--ci-ai, var(--ci-ai-ink)); font-size: .6rem; opacity: .45;
    transition: opacity var(--ci-transition);
}
.ci-drop-pill:hover .ci-drop-pill-spark, .ci-drop-pill.is-armed .ci-drop-pill-spark { opacity: .85; }
.ci-drop-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
/* Spins for the browser→circuit round trip after files are chosen, so the pill never looks inert. */
.ci-drop-pill-spin { width: 13px; height: 13px; flex-shrink: 0; border: 2px solid var(--ci-accent);
                     border-right-color: transparent; border-radius: 50%; display: inline-block;
                     animation: ci-drop-spin .7s linear infinite; }
@keyframes ci-drop-spin { to { transform: rotate(360deg); } }
.ci-drop-input input[type="file"] { width: 1px; height: 1px; }

/* A panel CLAIMED by a page drop zone (CiDropZone ScopeId), lit for as long as that zone is the live target — a
   record's file list is the example: the file lands here, and it has to say so from wherever the pointer is, not
   only from the strip at the top of it. The arbiter sets the attribute; Blazor owns the class attribute on these
   elements, which is why this is not a class. Outline, not border, so nothing on the panel moves as it lights. */
[data-ci-dropover] {
    outline: 2px solid var(--ci-accent); outline-offset: 3px;
    border-radius: var(--ci-radius); box-shadow: var(--ci-ring);
}

.ci-drop-overlay {
    position: fixed; inset: 0; z-index: 1400; background: rgba(20, 28, 46, .55);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
    display: flex; align-items: center; justify-content: center; pointer-events: none;
}
.ci-drop-overlay-card {
    display: flex; flex-direction: column; align-items: center; gap: .5rem; color: var(--ci-ink-on-brand);
    border: 2px dashed rgba(255,255,255,.75); border-radius: var(--ci-radius-lg); padding: 2.4rem 3.2rem;
    background: rgba(44, 64, 112, .45);
    /* The card reads as a real surface rather than a translucent rectangle, and lands rather than appears —
       this overlay now only shows when NO page zone wants the drop, so its arrival is meaningful. */
    box-shadow: 0 18px 50px rgba(8, 14, 28, .38), inset 0 1px 0 rgba(255,255,255,.14);
    animation: ci-drop-in .16s ease-out;
}
@keyframes ci-drop-in { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
.ci-drop-overlay-title { font-size: 1.25rem; font-weight: 700; }
.ci-drop-overlay-sub { font-size: .9rem; color: var(--ci-ink-on-brand); }
.ci-drop-overlay-card .ci-icon { animation: ci-drop-bob 1.5s ease-in-out infinite; }
@keyframes ci-drop-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
@media (prefers-reduced-motion: reduce) {
    .ci-drop-overlay-card .ci-icon { animation: none; }   /* the header spark no longer animates at all */
    .ci-dropm-card:hover, .ci-dropm-area:hover { transform: none; }
}

/* Action modal — the panel, its lilac header and its scrim are gone; it is a CiDialog like every other one.
   What remains is the BODY: the dropped-file list, the four action cards, the area picker and the search results.
   Their own paddings are dropped too, because CiDialog's body already insets them. */
.ci-dropm-files { display: flex; flex-direction: column; gap: .3rem; }
.ci-dropm-file { display: flex; align-items: center; gap: .5rem; font-size: .86rem; color: var(--ci-ink-body); padding: .3rem .5rem; border-radius: var(--ci-radius); }
.ci-dropm-file.selectable { cursor: pointer; }
.ci-dropm-file.selectable:hover { background: color-mix(in srgb, var(--ci-accent) 6%, var(--ci-surface)); }
.ci-dropm-file.failed { color: var(--ci-ink-faint); }
.ci-dropm-file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.ci-dropm-file-size { color: var(--ci-ink-quiet); font-size: .76rem; flex: none; }
.ci-dropm-file-status { flex: none; font-size: .76rem; font-weight: 700; color: var(--ci-ok); }
.ci-dropm-file-status.err { color: var(--ci-danger); font-weight: 600; }
.ci-dropm-spin {
    width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--ci-line); border-top-color: var(--ci-accent);
    animation: ci-dropm-rot .7s linear infinite;
}
@keyframes ci-dropm-rot { to { transform: rotate(360deg); } }
/* A COLUMN: the sentence, then the pair of choices. As one wrapping row the two radios were siblings of the
   prose, so the second dropped to its own line and left-aligned under it — reading as a stray option rather
   than the other half of a pair. */
.ci-dropm-note {
    margin: 6px 0 0; padding: .5rem .7rem; background: var(--ci-surface-2); border: 1px solid var(--ci-line);
    border-radius: var(--ci-radius); font-size: .8rem; color: var(--ci-ink-2);
    display: flex; flex-direction: column; gap: .35rem;
}
.ci-dropm-note-lead { color: var(--ci-ink-2); }
/* The choices wrap together or not at all, and stay aligned with each other when they do. */
.ci-dropm-choices { display: flex; flex-wrap: wrap; gap: .3rem 1.1rem; align-items: center; }
.ci-dropm-radio { display: inline-flex; align-items: center; gap: .3rem; font-weight: 600; cursor: pointer; }
.ci-dropm-cards { padding: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
.ci-dropm-card {
    display: flex; flex-direction: column; align-items: flex-start; gap: .3rem; text-align: left;
    padding: .95rem; border: 1px solid var(--ci-line); border-radius: var(--ci-radius-lg); background: var(--ci-surface); cursor: pointer;
    transition: border-color var(--ci-transition), box-shadow var(--ci-transition), background var(--ci-transition), transform var(--ci-transition);
}
.ci-dropm-card:hover { border-color: var(--ci-accent); background: var(--ci-surface-2); }
.ci-dropm-card.primary { background: linear-gradient(135deg, var(--ci-ai-soft), var(--ci-surface)); border-color: color-mix(in srgb, var(--ci-ai) 22%, var(--ci-surface)); }
.ci-dropm-card.primary:hover { border-color: var(--ci-ai); box-shadow: 0 8px 18px rgba(91,70,201,.18); }
/* icon in a rounded tinted tile (navy default; AI-purple on the VISDOM card) */
.ci-dropm-card-icon { display: inline-flex; align-items: center; justify-content: center; width: 2.1rem; height: 2.1rem;
    border-radius: var(--ci-radius); background: color-mix(in srgb, var(--ci-accent) 8%, var(--ci-surface)); color: var(--ci-brand-ink); margin-bottom: .1rem;
    transition: background var(--ci-transition), color var(--ci-transition); }
.ci-dropm-card:hover .ci-dropm-card-icon { background: var(--ci-hover); }
.ci-dropm-card.primary .ci-dropm-card-icon { background: linear-gradient(135deg, #5b46c9, #7c3aed); color: var(--ci-ink-on-brand); }
/* Disabled card (e.g. Create with VISDOM when the global VISDOM switch is off): greyed, not hidden. */
.ci-dropm-card.is-disabled { background: var(--ci-surface-sunk); border-color: var(--ci-line); cursor: not-allowed; opacity: .7; filter: grayscale(1); }
.ci-dropm-card.is-disabled:hover { border-color: var(--ci-line); box-shadow: none; transform: none; }
.ci-dropm-card.is-disabled .ci-dropm-card-icon { background: var(--ci-hover); color: var(--ci-ink-faint); }
.ci-dropm-card-title { font-weight: 700; font-size: .9rem; color: var(--ci-ink-body); }
.ci-dropm-card-tag { font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    color: var(--ci-ink-quiet); background: var(--ci-hover); border-radius: var(--ci-radius-sm); padding: .05rem .35rem; margin-left: .35rem; vertical-align: middle; }
.ci-dropm-card-sub { font-size: .76rem; color: var(--ci-ink-quiet); line-height: 1.3; }
.ci-dropm-section { display: flex; align-items: center; gap: .7rem; padding: 12px 0 4px; font-weight: 700; color: var(--ci-brand-ink); font-size: .9rem; }
.ci-dropm-back { background: transparent; border: 0; color: var(--ci-accent-ink); cursor: pointer; font-size: .82rem; font-weight: 600; padding: 2px 4px; }
.ci-dropm-back:hover { text-decoration: underline; }
.ci-dropm-areas { padding: 8px 0 12px; display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; }
.ci-dropm-area {
    display: flex; flex-direction: column; align-items: center; gap: .4rem; padding: .8rem .4rem;
    border: 1px solid var(--ci-line); border-radius: var(--ci-radius); background: var(--ci-surface); cursor: pointer; color: var(--ci-brand-ink);
    font-size: .8rem; font-weight: 600; transition: border-color var(--ci-transition), box-shadow var(--ci-transition);
}
.ci-dropm-area:hover { border-color: var(--ci-accent); box-shadow: 0 2px 8px rgba(44,64,112,.10); }
.ci-dropm-search {
    margin: 8px 0 4px; width: 100%; box-sizing: border-box; padding: .5rem .8rem; border: 1px solid var(--ci-line-strong);
    border-radius: var(--ci-radius); font-size: .88rem; background: var(--ci-surface-2);
}
.ci-dropm-search:focus { outline: none; border-color: var(--ci-accent); background: var(--ci-surface); box-shadow: var(--ci-ring); }
.ci-dropm-results { padding: 4px 0 12px; display: flex; flex-direction: column; }
.ci-dropm-result {
    display: flex; align-items: center; gap: .6rem; padding: .5rem .6rem; border: 0; background: transparent;
    border-radius: var(--ci-radius); cursor: pointer; text-align: left; font-size: .87rem; color: var(--ci-ink-body);
}
.ci-dropm-result:hover { background: color-mix(in srgb, var(--ci-accent) 6%, var(--ci-surface)); color: var(--ci-accent-ink); }
.ci-dropm-result-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ci-dropm-result-id { color: var(--ci-ink-quiet); font-size: .76rem; flex: none; }
.ci-dropm-empty { padding: 14px 2px 18px; color: var(--ci-ink-quiet); font-size: .85rem; }
.ci-dropm-foot { color: var(--ci-ink-quiet); font-size: .76rem; line-height: 1.4; }
.ci-dropm-foot a { color: var(--ci-accent-ink); text-decoration: none; font-weight: 600; }
.ci-dropm-foot a:hover { text-decoration: underline; }

@media (max-width: 1100px) {
    .ci-drop-pill-label { display: none; }        /* icon-only pill on narrow screens */
    .ci-drop-pill { padding: 0 .7rem; }
}
@media (max-width: 900px) {
    .ci-drop-pill { display: none; }              /* drop-anywhere + overlay still work via ciDrop */
    .ci-dropm-cards { grid-template-columns: 1fr; }
    .ci-dropm-areas { grid-template-columns: repeat(2, 1fr); }
}
.ci-acct-name { font-weight: 600; font-size: .88rem; max-width: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ci-acct-caret { color: var(--ci-ink-quiet); font-size: .7rem; }
.ci-acct-head { display: flex; align-items: center; gap: .6rem; padding: .8rem .9rem; border-bottom: 1px solid var(--ci-line); background: var(--ci-surface-2); }
.ci-acct-ident { min-width: 0; }
/* An email address has no spaces, so min-width:0 alone can't save it — without a break opportunity it runs past the
   244px panel edge and the domain is clipped off. Both lines may wrap mid-string: the whole address on two lines is
   worth more here than one tidy truncated line, since this block exists to say WHICH account you are signed into. */
.ci-acct-fullname { font-weight: 700; color: var(--ci-ink-body); font-size: .9rem; overflow-wrap: anywhere; }
.ci-acct-sub { color: var(--ci-ink-quiet); font-size: .76rem; margin-top: 1px; overflow-wrap: anywhere; line-height: 1.35; }
.ci-topbar-div { width: 1px; height: 22px; background: var(--ci-hover-strong); margin: 0 .15rem; }

@media (max-width: 720px) {
    .ci-hmenu-btn-label, .ci-acct-name { display: none; }
}

/* ───────────────────────────── Toasts (app-wide transient feedback) ─────────────────────────────
   BOTTOM-CENTRE (moved 2026-08-28, and what UX asked for). It has been round the houses: bottom-RIGHT originally,
   moved to top-centre 2026-08-03 because the VISDOM chat launcher is pinned bottom-right and covered it — but the
   top of a record page is the busiest strip in the app. The page header, the record's sticky tab strip and the
   toast all want the same band, so a toast raised while scrolled down landed on top of the pinned tabs (it never
   went BEHIND them — the stack is at the top of the z-scale — but covering the tabs is the same collision seen
   from the other side, and it is why a long-lived status message could not live up there at all).
   Bottom-centre is clear of both: nothing in the app's chrome is anchored bottom-centre, and the launcher keeps to
   the bottom-right corner, so the two only meet on a narrow viewport (handled below).
   `pointer-events: none` on the stack so it never blocks the page underneath; the toasts themselves take clicks
   back for their close button. */
.ci-toasts {
    position: fixed; bottom: 1.25rem; left: 50%; transform: translateX(-50%);
    z-index: var(--ci-z-toast);
    /* Column, not column-reverse: markup order is oldest-first, so the NEWEST toast sits nearest the bottom edge —
       closest to where the eye lands and where it animated in from — pushing older ones up out of the way. */
    display: flex; flex-direction: column; align-items: stretch; gap: .55rem;
    width: max-content; max-width: min(92vw, 480px); pointer-events: none;
}
.ci-toasts > * { pointer-events: auto; }
/* Narrow enough that a centred 92vw card reaches into the launcher's corner: stand the stack off the bottom so it
   clears the 52px FAB and its 22px inset. Only while the launcher is actually on screen (:has, same as .ci-content's
   gutter) — otherwise the toast floats for no reason on a page that doesn't have one. */
@media (max-width: 760px) {
    body:has(.vd-fab) .ci-toasts { bottom: calc(52px + 22px + .75rem); }
}

/* A toast is a headline + a sentence: the headline says WHAT happened, the line under it says what that means or
   what to do next. It is chromed like every other card in the app — a plain surface with a hairline — so the only
   coloured things are the icon and the edge stripe. (Until 2026-08-24 the whole card was a tint mixed from six hex
   literals, which both made it the loudest object on any screen and left it painting light colours in dark mode.) */
.ci-toast {
    position: relative; overflow: hidden;
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: .6rem; row-gap: .1rem;
    align-items: start;
    background: var(--ci-surface);
    border: 1px solid var(--ci-line); border-radius: var(--ci-radius);
    /* The kind reads from the edge, which is enough on a card that's already the only thing floating over the page. */
    border-left: 3px solid var(--ci-toast-c);
    box-shadow: var(--ci-shadow-lg);
    padding: .65rem .75rem; font-size: .8125rem; min-width: 19rem;
    animation: ci-toast-in .32s cubic-bezier(.16,1,.3,1);
}

/* The colour lives in variables so the stripe, the icon chip and the progress bar stay in step — a new kind is two
   declarations, not a new rule per part. Both are status TOKENS, so each kind inverts correctly in dark mode.
     --ci-toast-c     the kind's colour: stripe + icon + progress bar.
     --ci-toast-soft  the tint behind the icon only. */
.ci-toast-success { --ci-toast-c: var(--ci-ok); --ci-toast-soft: var(--ci-ok-surface); }
.ci-toast-error   { --ci-toast-c: var(--ci-danger); --ci-toast-soft: var(--ci-danger-surface); }
.ci-toast-warning { --ci-toast-c: var(--ci-warn); --ci-toast-soft: var(--ci-warn-surface); }
/* Info has no status token of its own: a tint of the accent is the formula, correct in both modes from one line. */
.ci-toast-info    { --ci-toast-c: var(--ci-accent-ink); --ci-toast-soft: color-mix(in srgb, var(--ci-accent) 12%, var(--ci-surface)); }

/* Spans both text rows so it optically centres against the whole block, not just the title. */
.ci-toast-icon {
    grid-row: 1 / span 2; align-self: center; justify-self: center;
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: 50%;
    background: var(--ci-toast-soft); color: var(--ci-toast-c);
}
.ci-toast-icon .ci-icon { stroke-width: 2.4; }

.ci-toast-title { grid-column: 2; color: var(--ci-ink-body); font-weight: 600; font-size: .875rem; line-height: 1.35; }
.ci-toast-msg { grid-column: 2; min-width: 0; line-height: 1.45; color: var(--ci-ink-support); }
/* On a neutral card a link is the app's link, same as anywhere else. */
.ci-toast-msg a { color: var(--ci-accent-ink); font-weight: 600; }

.ci-toast-close {
    grid-column: 3; grid-row: 1; align-self: start;
    display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
    background: transparent; border: 0; border-radius: 50%; color: var(--ci-ink-quiet); cursor: pointer; padding: 0;
    /* Legible but recessive: present enough that the way out is obvious on a toast that doesn't auto-dismiss. */
    opacity: .55; transition: opacity var(--ci-transition), background var(--ci-transition);
}
.ci-toast:hover .ci-toast-close, .ci-toast-close:focus-visible { opacity: 1; }
.ci-toast-close:hover { opacity: 1; background: var(--ci-hover); color: var(--ci-ink-body); }

/* Time remaining. Scales from the right so it drains toward the close button. */
.ci-toast-bar {
    position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: var(--ci-toast-c); opacity: .5; transform-origin: left center;
    animation-name: ci-toast-drain; animation-timing-function: linear; animation-fill-mode: forwards;
}
/* Hovering holds the toast open (ToastService.Hold), so the bar has to stop with it or it would lie. */
.ci-toast:hover .ci-toast-bar { animation-play-state: paused; }

/* Rises from below with a slight overshoot-free ease, matching the bottom-centre position — a panel enters from
   the edge it is anchored to. */
@keyframes ci-toast-in {
    from { opacity: 0; transform: translateY(14px) scale(.96); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes ci-toast-drain { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* Respect a reduced-motion preference: no slide, no drain animation. */
@media (prefers-reduced-motion: reduce) {
    .ci-toast { animation: none; }
    .ci-toast-bar { display: none; }
}

/* ───────────────────────────── Command palette (⌘K / Ctrl+K) ─────────────────────────────
   No header button — opened by the document-level Ctrl/Cmd+K listener registered in App.razor. */
.ci-cmdk-backdrop {
    position: fixed; inset: 0; z-index: 1200; background: rgba(20, 28, 46, .45);
    -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); animation: ci-cmdk-fade .12s ease-out;
}
.ci-cmdk {
    position: fixed; z-index: 1201; top: 14vh; left: 50%; transform: translateX(-50%);
    width: min(600px, 92vw); background: var(--ci-surface); border: 1px solid var(--ci-line); border-radius: var(--ci-radius-lg);
    box-shadow: 0 24px 60px rgba(16, 24, 40, .28), 0 4px 12px rgba(16, 24, 40, .12);
    overflow: hidden; animation: ci-cmdk-in .16s cubic-bezier(.2, .8, .3, 1);
}
.ci-cmdk-search { display: flex; align-items: center; gap: .6rem; padding: .85rem 1rem; border-bottom: 1px solid var(--ci-line); }
.ci-cmdk-search-icon { color: var(--ci-ink-faint); display: inline-flex; flex: none; }
.ci-cmdk-input { flex: 1; border: 0; outline: none; font-size: 1.05rem; color: var(--ci-ink-body); background: transparent; }
.ci-cmdk-input::placeholder { color: var(--ci-ink-faint); }
.ci-cmdk-list { max-height: min(52vh, 460px); overflow-y: auto; padding: .35rem; }
.ci-cmdk-empty { padding: 2rem 1rem; text-align: center; color: var(--ci-ink-faint); font-size: .9rem; }
.ci-cmdk-group {
    text-transform: uppercase; color: var(--ci-ink-faint); font-size: var(--ci-fs-label);
    font-weight: var(--ci-fw-label); letter-spacing: var(--ci-ls-label);
    padding: .6rem .7rem .3rem;
}
.ci-cmdk-item {
    display: flex; align-items: center; gap: .65rem; padding: .55rem .7rem; border-radius: var(--ci-radius); cursor: pointer;
    color: var(--ci-ink-body); scroll-margin: .5rem;
}
.ci-cmdk-item.sel { background: linear-gradient(90deg, var(--ci-surface-sunk), var(--ci-surface-2)); }
.ci-cmdk-item-icon {
    flex: none; width: 30px; height: 30px; border-radius: var(--ci-radius); display: inline-flex; align-items: center;
    justify-content: center; background: var(--ci-hover); color: var(--ci-brand-ink);
}
.ci-cmdk-item.sel .ci-cmdk-item-icon { background: var(--ci-accent); color: var(--ci-ink-on-brand); }
.ci-cmdk-item-create .ci-cmdk-item-icon, .ci-cmdk-item.sel .ci-cmdk-item-create { }
.ci-cmdk-item-label { flex: 1; min-width: 0; font-size: .92rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The characters the query actually matched. Weight and colour only — no background pill, because a fuzzy match can
   land on single scattered letters and a row of little highlighted boxes is harder to read than the plain label. */
.ci-cmdk-hit { background: none; color: var(--ci-accent-ink); font-weight: 800; }
.ci-cmdk-item.sel .ci-cmdk-hit { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.ci-cmdk-tag {
    font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--ci-ink-support);
    background: var(--ci-hover); border-radius: var(--ci-radius-sm); padding: .1rem .4rem; flex: none;
}
.ci-cmdk-item.sel .ci-cmdk-tag { background: var(--ci-hover-strong); color: var(--ci-accent-ink); }
.ci-cmdk-enter { flex: none; color: var(--ci-accent-ink); font-weight: 700; font-size: .9rem; }
.ci-cmdk-foot {
    display: flex; align-items: center; gap: 1rem; padding: .55rem .9rem; border-top: 1px solid var(--ci-line);
    background: var(--ci-surface-2); color: var(--ci-ink-faint); font-size: .72rem;
}
.ci-cmdk-foot-brand { margin-left: auto; font-weight: 700; color: var(--ci-ink-disabled); letter-spacing: .02em; }
.ci-cmdk kbd, .ci-cmdk-esc {
    display: inline-flex; align-items: center; justify-content: center; min-width: 1.3em; padding: .05rem .35rem;
    background: var(--ci-surface); border: 1px solid var(--ci-line); border-bottom-width: 2px; border-radius: var(--ci-radius-sm); font-size: .72rem;
    font-family: inherit; color: var(--ci-ink-support); line-height: 1.4;
}
/* The key cap in the query box is a button (it closes the palette), so it has to look pressable. */
.ci-cmdk-esc { flex: none; cursor: pointer; }
.ci-cmdk-esc:hover { border-color: var(--ci-line-strong); background: var(--ci-hover); color: var(--ci-ink-body); }
.ci-cmdk-esc:active { border-bottom-width: 1px; transform: translateY(1px); }
.ci-cmdk-esc:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 1px; }
@keyframes ci-cmdk-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ci-cmdk-in { from { opacity: 0; transform: translateX(-50%) translateY(-8px) scale(.98); } to { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); } }

/* ───────────────────────────── Responsive shell (mobile / tablet) ─────────────────────────────
   Below ~900px the fixed sidebar becomes an off-canvas drawer: a hamburger in the top bar slides it in over a
   dimmed backdrop; tapping a link or the backdrop dismisses it. Above the breakpoint the layout is unchanged. */
.ci-navburger {
    display: none; align-items: center; justify-content: center; background: transparent;
    border: 1px solid transparent; border-radius: var(--ci-radius); padding: .35rem; color: var(--ci-slate); cursor: pointer;
}
.ci-navburger:hover { background: var(--ci-hover); color: var(--ci-accent-ink); }
.ci-nav-backdrop { display: none; }

@media (max-width: 900px) {
    .ci-navburger { display: inline-flex; }
    /* Full drawer on mobile — there is no rail to collapse, so the brand goes back to being just the brand. */
    .ci-brandbtn { pointer-events: none; }
    .ci-brand-chev { display: none; }
    .ci-sidebar {
        position: fixed; top: 0; left: 0; height: var(--ci-vh); width: var(--ci-sidebar-w);
        transform: translateX(-100%); transition: transform .22s ease; z-index: 1300;
    }
    .ci-sidebar.collapsed { width: var(--ci-sidebar-w); }
    .ci-shell.nav-open .ci-sidebar { transform: translateX(0); box-shadow: 6px 0 30px rgba(16, 24, 40, .34); }
    .ci-nav-backdrop {
        display: block; position: fixed; inset: 0; background: rgba(20, 28, 46, .5);
        z-index: 1250; animation: ci-cmdk-fade .15s ease-out;
    }
    .ci-searchbtn { max-width: 40vw; }
    .ci-content { padding: 1rem; }
}
/* ───────────────────────── Compact top bar on smaller screens (laptops / demo machines) ─────────────────────────
   The header has grown a lot of controls (search, drag-drop, New, notifications, announcements, tutorials, help,
   confidentiality, language, account). At 1920 they sit comfortably; on the ~1440-1536 laptops sales demos run on,
   the row is tight and the account name is usually what gets squeezed.

   Scaled by VARIABLES rather than per-control overrides: --ci-ctl-h drives every control's height and
   --ci-hdr-icon every glyph inside them, so one breakpoint takes the whole row down ~80% and any control added
   later inherits it for free. Only sizing changes here — nothing is hidden, so a demo shows the same UI. */
@media (max-width: 1600px) {
    :root {
        --ci-topbar-h: 48px;
        --ci-ctl-h: 30px;
        --ci-hdr-icon: 18px;
    }
    .ci-searchbtn { width: 190px; font-size: .82rem; padding: 0 .5rem 0 .8rem; }
    .ci-searchbtn-kbd { font-size: .64rem; }
    .ci-hmenu-btn { padding: 0 .7rem; font-size: .82rem; }
    .ci-acct-name { font-size: .82rem; max-width: 9rem; }
    /* The button drops to 30px here, so the disc comes down with it to keep the same ring of space around it. */
    .ci-acct-btn > .ci-acct-avatar { width: 23px; height: 23px; font-size: .7rem; }
    .ci-drop-pill { font-size: .8rem; }
    .ci-topbar { padding: 0 .9rem; }
    .ci-topbar-right { gap: .5rem; }
}

/* ── Compact tier: laptops ──────────────────────────────────────────────────────────────────────
   The tier above already steps the TOP BAR down; this steps the page BODY down, which is what still
   read as oversized on a company laptop.
   Measured against the real target: a 1600×900 panel at Windows 125% reports a 1250×722 CSS viewport.
   Note it is barely narrow but a LOT shorter — a width-only breakpoint at 1280 would sail straight past
   it, which is why max-height is in the query. Height is the honest signal for this class of machine.
   Done with one root font-size rather than a pile of per-component overrides: the type scale and nearly
   all spacing are in rem, so this carries them together and anything added later inherits it free.
   Deliberately NOT implemented with `zoom`. Zoom is already the per-user Account → Zoom preference
   (--ci-zoom); driving it from CSS too would multiply against whatever the user picked, and one uniform
   multiplier cannot say "text a little smaller, chrome a lot smaller, hit targets unchanged" — which is
   exactly the distinction that makes a dense screen usable rather than merely smaller. Layering also
   composes properly this way: the device picks a sensible default, the user still nudges from there.
   Expressed as a PERCENTAGE, not px, and it is the one place a non-rem value is correct: rem is defined against
   the root, so the root is the anchor everything else is relative to and cannot itself be rem. A percentage beats
   a px literal here because it resolves against the BROWSER's font size — someone who has set their browser to
   20px for legibility keeps that advantage instead of having it overwritten. 87.5% ≈ 14px at a default 16px root. */
@media (max-width: 1400px), (max-height: 820px) {
    html { font-size: 87.5%; }
    :root {
        /* Overrides for the things that must NOT shrink proportionally. Field labels are already the smallest
           text in the app; at a 14px root .72rem lands near 10px, so the token goes UP to hold roughly the
           same rendered size. Shrinking a caps label proportionally is how it stops being read. */
        --ci-fs-label: .78rem;
        /* Reclaimed for content: on a 1250px viewport the rail was taking a fifth of the width. */
        --ci-sidebar-w: 224px;
    }

    /* Syncfusion grid density. This has to be done by hand: the theme hardcodes grid metrics in PIXELS
       (.e-rowcell is font-size:14px/line-height:22px/padding:6px 8px), so they are the one part of the UI that
       does NOT follow the root font-size — which is why grids stayed full-size while everything around them
       shrank. Syncfusion's own `.e-small` density class is no help here either: in v33.2.13 the theme has
       .e-small rules for inputs, dropdowns and checkboxes but NOT ONE for .e-grid, so it would have shrunk the
       form controls and left every grid untouched — worse than doing nothing.
       Row box goes ~35px → ~27px. Our own row-action buttons inside the cells are in rem and have already
       scaled, so this is the last piece that was out of step. */
    .e-grid .e-rowcell { font-size: 13px; line-height: 18px; padding: 4px 8px; }
    .e-grid .e-headercell { padding: 4px 8px; }
    .e-grid .e-headercelldiv { font-size: 13px; line-height: 16px; }
}

@media (max-width: 560px) {
    /* Very small screens: drop the label, keep a round icon-only button — search stays reachable, unlike the old
       box which was hidden outright. */
    .ci-searchbtn { width: var(--ci-ctl-h); padding: 0; justify-content: center; }
    .ci-searchbtn-label, .ci-searchbtn-kbd { display: none; }
    .ci-topbar { padding: 0 .75rem; }
    .ci-topbar-right { gap: .5rem; }
}

/* ───────────────────────────── Skeleton loaders (shimmer while data loads) ───────────────────────────── */
.ci-skel { position: relative; overflow: hidden; background: var(--ci-hover); border-radius: var(--ci-radius-sm); }
.ci-skel::after {
    content: ""; position: absolute; inset: 0; transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent); animation: ci-shimmer 1.3s infinite;
}
@keyframes ci-shimmer { 100% { transform: translateX(100%); } }
.ci-skel-line { height: .78rem; border-radius: var(--ci-radius-sm); }
.ci-skel-lines { display: flex; flex-direction: column; gap: .7rem; padding: .5rem .25rem; }
.ci-skel-grid { border: 1px solid var(--ci-line); border-radius: var(--ci-radius); overflow: hidden; background: var(--ci-surface); }
.ci-skel-gridhead { height: 2.4rem; border-radius: 0; background: var(--ci-hover-strong); }
.ci-skel-gridrow { height: 2.05rem; border-radius: 0; margin: .5rem .6rem; }
.ci-skel-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem 1.5rem; padding: .25rem; }
.ci-skel-field { display: flex; flex-direction: column; gap: .4rem; }
.ci-skel-input { height: 2.2rem; }
@media (prefers-reduced-motion: reduce) { .ci-skel::after { animation: none; } }
/* Static placeholder (Skeleton Static="true"): an inert outline of a shape that is NOT loading — a shimmer there
   reads as a stalled request rather than "your turn". Used by the add form while it waits for a record type. */
.ci-skel-static .ci-skel::after { display: none; }
.ci-skel-static .ci-skel { background: var(--ci-surface-sunk); }

/* Top padding is --ci-scroll-pad because it is not free spacing: it is the band of page content that stays
   visible as things scroll, so it sets how far the sticky tab strips have to offset and how tall their covers
   must be. Lowering it is what buys a tighter page header — see the token. */
.ci-content { flex: 1; background: var(--ci-content-bg); padding: var(--ci-scroll-pad) 1.25rem; overflow: auto; min-height: 0; }
/* Reserve bottom clearance for the fixed VISDOM chat FAB — but ONLY while its collapsed pill (.vd-fab) is actually
   shown, so there's no dead whitespace when the chat is open, suppressed on a page, or admin-disabled. Pure CSS
   (:has) — auto-reacts as the FAB appears/disappears; degrades to no gutter on browsers without :has(). */
body:has(.vd-fab) .ci-content { padding-bottom: var(--ci-fab-gutter); }

/* …and take the launcher AWAY while a drawer is open. Two reasons, one of which is a real bug: a drawer is modal,
   so a floating button that stays clickable over its scrim isn't reachable-by-design, it's a hole in the modality.
   The other is stacking — a drawer opened from a component that lives INSIDE the header (the profile menu's "Ask an
   administrator" panel) sits in the header's stacking context, so its z-index is resolved against that ancestor and
   the fixed launcher paints over its footer buttons however high the drawer's own z-index goes. Hiding the launcher
   fixes it for every drawer at once, wherever it was opened from, instead of chasing the ancestor per host. */
body:has(.ci-drawer) .vd-fab { display: none; }

/* The guided-tutorial builder is a slide-in panel with a footer too, but a bespoke one (.ci-tut-drawer, not
   CiDrawer), so the rule above never covered it — and the launcher sat squarely over its "Save and next" button,
   which is unreachable rather than merely ugly. Named explicitly instead of being converted to CiDrawer, because
   this panel deliberately hides itself while the author is picking an element on the page. */
body:has(.ci-tut-drawer) .vd-fab { display: none; }

/* The temporary-system-lockout strip, between the top bar and the page (LockoutBanner). Part of the shell rather
   than a page, so it lives here: it has to sit OUTSIDE .ci-content's scroller, or it would scroll away with the
   page and stop being the standing reminder it exists to be. flex-shrink:0 for the same reason every other piece
   of shell furniture carries it — the content area claims the height that is left, not this. */
.ci-lockbar { display: flex; align-items: center; gap: .45rem; flex-shrink: 0; padding: .4rem 1.25rem;
              background: var(--ci-danger-surface); border-bottom: 1px solid var(--ci-line-strong); color: var(--ci-danger);
              font-size: var(--ci-fs-meta); }
.ci-lockbar-t { font-weight: 600; }
.ci-lockbar-a { color: inherit; text-decoration: underline; }
.ci-lockbar-a:hover { color: var(--ci-danger); }

/* List pages fill the content area as a flex column so the grid can take the remaining height and scroll
   internally (instead of the whole page scrolling). The page-head stays fixed at the top. */
.ci-list-wrap { display: flex; flex-direction: column; height: 100%; }
.ci-list-wrap .page-head { flex-shrink: 0; }
.ci-list-wrap > .ci-phead { flex-shrink: 0; }
/* Chat-launcher clearance on a full-height list page: an OFF-SCREEN runway, not an on-screen gutter.
   Everywhere else the clearance is .ci-content's padding-bottom, which is right for a page that scrolls naturally.
   It is wrong here, twice over: height:100% resolves against the scroller's CONTENT box, so that padding shortened
   the grid AND left a dead band of page colour under the card — the grid stopped short of the bottom while the
   space it gave up sat there empty. And it still didn't help, because with zero overflow there was nothing to
   scroll, so whatever the launcher covered (the pager, the last row's actions) stayed covered with no way out.
   Swapping the padding for a bottom MARGIN moves that same clearance below the fold: the grid now fills the full
   visible height on arrival, and the page has exactly one launcher's worth of travel to lift the pager clear.
   Both rules are scoped to :has(.vd-fab), so a page with no launcher gets neither an idle scrollbar nor a gutter.
   The padding drops to --ci-scroll-pad rather than to 0: at 0 the card ran flush into the bottom edge of the
   window and looked cropped rather than full. This matches the scroller's own top padding, so the card sits in an
   even margin top and bottom — enough to read as deliberate, far too little to be the dead band it replaced. */
body:has(.vd-fab) .ci-content:has(.ci-list-wrap) { padding-bottom: var(--ci-scroll-pad); }
body:has(.vd-fab) .ci-list-wrap { margin-bottom: var(--ci-fab-gutter); }
/* The launcher also bites into the pager SIDEWAYS. A list page's pager pins its "1 of 865 pages (43,207 items)"
   flush to the right edge of the grid, and the button overhangs that edge — so on arrival the tail of the count is
   behind it, and the runway above only uncovers it once you have scrolled. The count is the first thing anyone
   checks after a filter ("did that actually narrow it?"), so it should not need a scroll to be read. Reserving the
   launcher's width at the end of the pager moves the text clear and shifts nothing else: the pager's own row is
   half empty, and the page-size picker and page numbers sit far to its left. */
body:has(.vd-fab) .ci-list-wrap .e-gridpager .e-parentmsgbar { margin-right: var(--ci-fab-clear); }

/* CiDataGrid pager layout (bug 45041). Syncfusion floats its three parts - the page buttons, the page-size picker,
   the "1 of 248 pages" count - and in a narrow grid (a standard-width drawer) the eight numbers wrapped under the
   arrows and the picker landed on top of them. A grid keeps them in order (the count comes BEFORE the picker in the
   DOM): buttons | picker | count pushed right, one row, as a wide list has always looked. The pager is its own size
   container, so wherever it is narrow it goes to two clean rows - the buttons, then picker + count - and sheds what
   doesn't fit rather than wrapping: "Items per page" first (the 50 ▾ says it), then « and » (‹ › and the numbers
   still reach every page). */
/* CiDataGrid's footer row of column totals (ColumnTotals, bug 46236): bold figures on the sunk tint, read as a total. */
.ci-grid-host .e-summaryrow .e-summarycell { background: var(--ci-surface-sunk); border-top: 1px solid var(--ci-line-strong); }
.ci-grid-host .ci-grid-foot { font-weight: 700; color: var(--ci-brand-ink); }
.ci-grid-host .e-gridpager.e-pager { container-type: inline-size; display: grid; grid-template-columns: auto auto 1fr;
                                     align-items: center; gap: .4rem .6rem; }
.ci-grid-host .e-gridpager .e-pagercontainer { order: 1; margin: 0; }
.ci-grid-host .e-gridpager .e-pagesizes { order: 2; display: inline-flex; align-items: center; gap: .4rem; margin: 0; height: auto; }
/* Syncfusion lifts the picker 24px to line it up in its float layout - in a grid row it then rides up over the row above. */
.ci-grid-host .e-gridpager .e-pagesizes .e-pagerdropdown { margin-top: 0; }
.ci-grid-host .e-gridpager .e-pagesizes .e-pagerconstant { margin: 0; white-space: nowrap; }
.ci-grid-host .e-gridpager .e-parentmsgbar { order: 3; justify-self: end; float: none; white-space: nowrap; }
@container (max-width: 900px) {
    .ci-grid-host .e-gridpager .e-pagesizes .e-pagerconstant { display: none; }
}
/* Two rows when the buttons can't share a row: a mid-width grid (a record dock) only once it has five or more page
   numbers - one page of results there still fits on one row - and a narrow one (a drawer) always. */
@container (max-width: 760px) {
    .ci-grid-host .e-gridpager:has(.e-numericcontainer > :nth-child(5)) .e-pagercontainer { grid-column: 1 / -1; justify-self: start; }
    .ci-grid-host .e-gridpager:has(.e-numericcontainer > :nth-child(5)) .e-parentmsgbar { grid-column: 2 / -1; }
}
@container (max-width: 480px) {
    .ci-grid-host .e-gridpager .e-pagercontainer { grid-column: 1 / -1; justify-self: start; }
    .ci-grid-host .e-gridpager .e-parentmsgbar { grid-column: 2 / -1; }
    .ci-grid-host .e-gridpager .e-pagercontainer .e-first,
    .ci-grid-host .e-gridpager .e-pagercontainer .e-last { display: none; }
}
.ci-grid-host .e-gridpager .e-pagercontainer { display: flex; align-items: center; flex-wrap: nowrap; }
.ci-grid-host .e-gridpager .e-numericcontainer { display: flex; flex-wrap: nowrap; }

/* ───────────────────────────── Canonical page header (CiPageHeader) ─────────────────────────────
   One header style for every non-hero page: title is always topmost; optional right-aligned actions on the
   same row; optional description below the title; tabs/filters/switchers belong BELOW this block, never above.
   The single variation is `ci-phead--ai` (VISDOM/AI tools) which tints the title purple. */
/* The gap to whatever follows (usually the sticky tab strip). Was a hardcoded 1.25rem, set when the title was
   24px/800; at 22px/700 that read as the header drifting away from its own page. It is --ci-phead-gap now —
   see the token for the invariant it shares with the tab strip's shield, which is what makes this the tightest
   value that is actually safe. The title's line box adds a few px of its own below the glyphs on top of it. */
/* Three stacked lanes: the trail, then the title ROW (title + actions), then the description. The header used to be
   ONE flex row of [trail+title+description | actions] with align-items:flex-start, which aligned the action buttons
   to the top of that block — i.e. to the 12px grey breadcrumb, with the 24px title sitting below them and nothing on
   the row sharing a baseline. Reported 2026-09-06; the fix is to give the trail its own lane so the buttons can align
   to the title instead, which is the largest thing on the screen and the thing they actually act on. */
.ci-phead { display: block; margin-bottom: var(--ci-phead-gap); }
/* align-items:center rather than flex-start so a title that WRAPS to two lines centres its buttons against the whole
   block instead of letting them ride up to the first line.
   It carries THREE items — title, actions, description — and the description takes a full-width basis so it always
   falls to its own line. Explicit `order` on all three is what lets the narrow tier below put the actions AFTER the
   description without moving anything in the markup. */
.ci-phead-row { display: flex; align-items: center; justify-content: space-between; gap: .5rem 1.5rem; flex-wrap: wrap; }
.ci-phead-row > .ci-phead-title { order: 1; }
.ci-phead-row > .ci-phead-actions { order: 2; }
/* The actions ride on the title's FIRST line, not the middle of the block (owner's call 2026-10-05, replacing the
   centring described above): a long record name wraps to two or three lines, and centred buttons floated beside
   none of them. The margin centres the buttons on one title line, so a single-line title looks the same as before. */
.ci-phead-row { align-items: flex-start; }
.ci-phead-row > .ci-phead-actions { margin-top: calc((var(--ci-fs-page) * 1.35 - var(--ci-btn-h, 36px)) / 2); }
/* `flex: 0 0 100%` AND `max-width:none`: a max-width on a flex item clamps the hypothetical main size that flex
   line-breaking measures, so the old `flex-basis:100%; max-width:82ch` pair resolved to a 574px item that fitted
   happily on the title's line — the description sat beside the title, the h1 shrank until a record's badges wrapped
   into a column, and the action buttons stopped short of the right edge. The 82ch reading cap now lives on the inner
   .ci-phead-desc-in, which is not a flex item and can be clamped freely. */
.ci-phead-row > .ci-phead-desc { order: 3; flex: 0 0 100%; max-width: none; margin: 0; }
.ci-phead-desc-in { max-width: 82ch; margin: 0; }
/* Breadcrumb trail (CiPageHeader, derived from the route — see BreadcrumbService). It renders ONLY when it carries a
   real parent, never as a bare "Home ›" and never repeating the heading below it — so when it does appear it is worth
   reading, and it is sized to be read rather than squinted at. Still clearly subordinate to the 1.5rem title.
   It gets REAL air beneath it (2026-09-06). It used to carry margin:-.15rem 0 .45rem — a negative top pull into the
   header's own leading so the trail cost ~16px rather than a full line — and that saving was not worth what it cost:
   at .45rem the trail read as the title's first line rather than as a path above it. Headers are ~10px taller now,
   which is the whole price of the change. */
.ci-crumbs { display: flex; align-items: center; flex-wrap: wrap; gap: .3rem; margin: 0 0 .9rem;
    font-size: .75rem; line-height: 1.4; min-width: 0; }
/* Quieter than --ci-slate was. Three lines stack in this header — trail, title, meta — and when the trail sat at
   slate/500 it carried nearly the title's colour weight, so the block read as one paragraph. The trail is context:
   it should be legible on purpose, not on arrival. The LINK keeps its accent, so the part you can act on is the
   part that stands out. */
.ci-crumb { color: var(--ci-ink-quiet); font-weight: 500; max-width: 28ch; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
/* A parent you can go back to is the whole point of the trail, so it looks clickable rather than merely present. */
.ci-crumb-link { color: var(--ci-accent-ink); font-weight: 600; text-decoration: none;
    border-bottom: 1px solid transparent; transition: color var(--ci-transition), border-color var(--ci-transition); }
.ci-crumb-link:hover { color: var(--ci-accent-ink); border-bottom-color: currentColor; }
.ci-crumb-sep { color: var(--ci-ink-faint); font-size: .85rem; line-height: 1; }
/* Where you are. Present so the trail reads as a path rather than a stranded parent link, but not a link — it would
   go nowhere — so no link affordance and no hover.
   Was navy/600. That is what made the header blend: the tail sat in the title's own colour at a heavy weight,
   directly above the title, so the eye took the two lines as one block. It repeats the heading BY DESIGN (every
   breadcrumb does), which only works if it is unmistakably subordinate — hence the quiet grey and the lighter
   weight than the parent links beside it. The parents stay more prominent than the tail: they are the ones you
   can actually click. */
.ci-crumb-current { color: var(--ci-ink-faint); font-weight: 400; cursor: default; }
/* The AI variation tints its heading purple; the trail follows so the pairing doesn't clash. */
.ci-phead--ai .ci-crumb-link { color: var(--ci-ai-ink); }
.ci-phead--ai .ci-crumb-link:hover { color: var(--ci-accent-ink); }
/* The page step. Was 24px/800 — heavy enough that the title read as a marketing hero while everything
   under it read as a utility; 700 holds the same authority without the change of voice. */
/* The page step. Was 24px/800 — heavy enough that the title read as a marketing hero while everything under it
   read as a utility; 700 holds the same authority without the change of voice.
   line-height is 1.35, not the 1.25 it started at: Inter seats descenders close to the bottom of the line box, and
   the tab strip's shield lands exactly on this element's bottom edge, so at 1.25 the tail of a "g" or "q" in a
   record title got shaved. The clearance lives INSIDE the h1, which costs less than the empty description box
   collapsed just below — net, the header sits tighter to the page than before, not looser.
   flex-wrap: a long record title used to SHRINK to make room for its badge and break across two lines, leaving the
   badge floating in the vertical middle of the gap beside it — the badge is what wants a new line there, not the
   name. Wrapping only engages where the row already overflowed, so a title that fits is untouched; row-gap keeps
   the dropped badge tucked under the title rather than a full word-space away. */
/* Both a flex CONTAINER (icon, title, count, badges, title actions) and a flex ITEM of .ci-phead-row — the
   flex/min-width pair is what used to sit on the .ci-phead-main wrapper this replaced. min-width:0 lets a long
   record name shrink rather than shoving the actions off the row. overflow-wrap:anywhere finishes that job for a
   name with no spaces in it: without it the name's span keeps its full width and slides under the actions
   (bug 46949). It only breaks a word that can't fit, so ordinary titles are unchanged. */
.ci-phead-title { display: flex; align-items: center; flex-wrap: wrap; gap: .55rem; row-gap: .3rem; margin: 0; color: var(--ci-ink);
    flex: 1 1 16rem; min-width: 0; overflow-wrap: anywhere;
    font-size: var(--ci-fs-page); font-weight: var(--ci-fw-page); letter-spacing: var(--ci-ls-page); line-height: 1.35; }
.ci-phead-title .ci-icon { color: var(--ci-brand-ink); flex: none; }
/* CiPageHeader Registered="true" — a title ending in a product name that carries a registered mark (VISDOM,
   Contract Insight). Drawn, not typed into the Title string: at this heading's size a literal ® is a full-weight
   character sitting beside the name rather than a mark on it. Same treatment as the nav rail's AI row, so the two
   places the product is named agree. */
.ci-phead-marked::after { content: "\00ae"; font-size: .5em; font-weight: 600; vertical-align: .75em; }
/* Sized in em for the same reason as the title-line badge below it: this pill sits ON the heading, so it is
   measured FROM the heading and tracks it at every size instead of being a fixed chip beside a scaling title.
   Same font-size/padding as .ci-phead-badges .ci-badge, and both carry a 1px border, so the record count on a
   list page and the type/ID badge on a record page come out the identical height — they are the same kind of
   thing in the same position, and used to differ only because two rules were written at different times. */
.ci-phead-count { display: inline-flex; align-items: center; align-self: center; font-size: .46em; font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase; color: var(--ci-brand-ink); background: var(--ci-surface-sunk);
    border: 1px solid var(--ci-line); border-radius: var(--ci-radius-pill); padding: .55em .8em; line-height: 1; white-space: nowrap;
    /* Nudge to the heading's OPTICAL centre — the title's glyphs sit high in their line box, so a geometric
       center leaves the pill looking a touch high. 1px rather than the old 2px: that was calibrated against
       a 24px/800 title, and both the lighter weight and Inter's taller x-height close most of the gap. */
    position: relative; top: 1px; }
.ci-phead-desc { margin: .4rem 0 0; color: var(--ci-ink-support); font-size: .88rem; line-height: 1.5; max-width: 82ch; }
.ci-phead-desc :last-child { margin-bottom: 0; }
/* A page that passes a DescriptionContent slot which renders NOTHING still gets the wrapper — CiPageHeader can only
   test whether the fragment was supplied, not whether it drew anything, and the record page always supplies one
   (its subtitle is conditional). That left an empty box holding .4rem of margin under every record title: dead
   space that read as "the header has drifted away from the page". Collapse it when it has no content. */
.ci-phead-desc:empty, .ci-phead-desc:has(> .ci-phead-desc-in:empty) { display: none; }
/* The double-click-copy confirmation: a small inverse chip at the pointer, gone in under a second. Inverse (body
   ink as ground, surface as text) so one declaration reads in both themes. */
.ci-copybubble { position: fixed; z-index: 4000; background: var(--ci-ink-body); color: var(--ci-surface);
    font-size: .72rem; font-weight: 600; line-height: 1; padding: .32rem .55rem; border-radius: var(--ci-radius-sm);
    pointer-events: none; opacity: 0; transform: translateY(2px);
    transition: opacity var(--ci-transition), transform var(--ci-transition); }
.ci-copybubble.on { opacity: 1; transform: none; }
/* NO cursor of its own on a copyable value, and that is a decision rather than an omission (pointer audit,
   2026-09-06; tried `copy`, reverted the same day).
   Three reasons it went back:
     · `copy` means "dropping here will copy" — it is a drag-and-drop cursor. Double-click-to-copy is a stretch of
       it, not its meaning.
     · It fired on all 26 values of a record at once. One badge-y cursor per field, down a whole form, is chatter
       in a tool people read all day.
     · It bought less than it looked like. These values are ordinary selectable text, so the default `auto`
       already resolves to an I-beam over the glyphs — which is both true and the very gesture the shortcut
       extends, since double-clicking text is a text gesture.
   If real discoverability is ever wanted, the answer is a hover-revealed copy icon on the FEW fields worth it
   (ids, reference numbers), not a cursor on every value. */

/* The term-countdown pill. ★ DORMANT since 2026-09-06 — nothing renders one: it came off the record sidebar's
   summary card (owner: the chip read as out of place among that card's quiet label/value pairs, and repeated the
   date above it). Kept with its helpers in RecordEditView; see the ExpiresPill block there for how to re-attach it,
   and delete both together if it is still unused when you read this. */
.ci-fact-pill { display: inline-block; font-size: .7rem; font-weight: 700; border-radius: var(--ci-radius-pill);
    padding: .12rem .55rem; white-space: nowrap; line-height: 1.3; }
/* As a link (to the module's renewal-window view) the pill keeps its chip look; press answers with brightness,
   never movement. */
a.ci-fact-pill { text-decoration: none; cursor: pointer; }
a.ci-fact-pill:hover { filter: brightness(.95); }
.ci-fact-pill.warn { background: var(--ci-warn-surface); color: var(--ci-warn); border: 1px solid var(--ci-warn-line); }
.ci-fact-pill.late { background: var(--ci-danger-surface); color: var(--ci-danger);
    border: 1px solid color-mix(in srgb, var(--ci-danger) 30%, var(--ci-surface)); }
.ci-fact-pill.expired { background: var(--ci-danger); color: var(--ci-ink-on-brand); border: 1px solid var(--ci-danger); }

/* ── The header's narrow tiers. Width-based on purpose (the company-laptop tier is height-based and lives in the
   root font ladder — see the compact tier): these engage on genuinely narrow windows — a tablet, a phone, a
   half-snapped window — where the flex-wrap fallbacks alone left the trail wrapping to three lines and the
   actions row starting mid-air. ── */
@media (max-width: 760px) {
    .ci-phead-row { gap: .6rem 1rem; }
    /* The trail becomes ONE line that ellipsizes — three wrapped crumb lines above a title is where the
       "bleeding into the header" feeling comes from on small screens. It also gives back some of the air the
       wide tier spends: at this width the actions have already dropped to their own row, so the header is
       carrying an extra line regardless and the trail's lane can afford to be tighter. */
    .ci-crumbs { flex-wrap: nowrap; overflow: hidden; margin-bottom: .6rem; }
    .ci-crumb { max-width: 24vw; }
    .ci-crumb-current { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .ci-phead-title { font-size: calc(var(--ci-fs-page) * .9); }
    /* Actions take their own full row (they were wrapping there anyway) and stay right-aligned like all chrome.
       They also swap places with the description, so the order is title, subline, buttons. Without the swap the
       buttons land BETWEEN a record's title and its own "vendor · updated by" line, splitting the heading from the
       text that belongs to it — reported 2026-09-06, on a phone, the day the row was introduced. */
    .ci-phead-actions { flex-basis: 100%; }
    .ci-phead-row > .ci-phead-actions { margin-top: 0; }
    .ci-phead-row > .ci-phead-desc { order: 2; }
    .ci-phead-row > .ci-phead-actions { order: 3; }
}
@media (max-width: 480px) {
    .ci-phead-title { font-size: calc(var(--ci-fs-page) * .82); }
}
/* Header actions stay pinned to the RIGHT — even when they wrap to their own row under a long title (a package
   console can carry 5-7 of them). margin-left:auto right-aligns the block in the flex row; justify-content keeps the
   buttons right-aligned within it once wrapped, so they never drift back under the title left-aligned. */
.ci-phead-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
/* U11: the primary ends up rightmost whatever order the page declared, exactly as .ci-dlg-foot / .ci-drawer-foot
   already do. The header was the one chrome row with no ordering of its own, so six of them had drifted. A danger
   button is deliberately NOT ordered last — destructive is never the row's commit; it sits among the secondaries
   (the record header's Back · Delete · Actions · Edit All). */
.ci-phead-actions > * { order: 1; }
.ci-phead-actions > .ci-hbtn-primary, .ci-phead-actions > .ci-btn-primary { order: 2; }
/* The one variation — AI/VISDOM tools: a subtle AI-purple gradient on the title text (icon stays solid). */
.ci-phead--ai .ci-phead-title .ci-icon { color: var(--ci-ai-ink); }
/* ── Inline alerts ──────────────────────────────────────────────────────────────────────────────
   .ci-alert is used across dozens of pages, but it was only ever defined in the <style> block of SOME of them — so on
   a page that forgot it, an alert rendered as an unstyled div, and an alert with an empty message rendered as nothing
   at all (which is how an "add survey" drawer came out completely blank instead of explaining a refusal). Defined here
   so the class always draws something; pages carrying their own copy still win, being later in the cascade. */
.ci-alert { background: var(--ci-hover); border: 1px solid var(--ci-line-strong); color: var(--ci-ink-2); padding: .7rem .9rem;
    border-radius: var(--ci-radius); font-size: .88rem; line-height: 1.45;
    /* An alert is a block that introduces what follows, so it owns the gap under itself. Every page that defined its
       own .ci-alert was really only adding this — which is how they all ended up with subtly different radii and
       border colours too. Set it here and pages can drop their copy. */
    margin: 0 0 1rem; }
.ci-alert:empty::before { content: "Something went wrong, and no reason was given."; }
.ci-alert-error { background: var(--ci-danger-surface); border-color: var(--ci-line-strong); color: var(--ci-danger); }
.ci-alert-warn { background: var(--ci-warn-surface); border-color: var(--ci-line); color: var(--ci-warn); }
.ci-alert-ok { background: var(--ci-surface-2); border-color: var(--ci-line-strong); color: var(--ci-ok); }

/* ── Record sidebar widget chrome ───────────────────────────────────────────────────────────────
   Shared by every sidebar card (notices, stage, risk, status history) so they read as one column of
   widgets rather than five separately-styled boxes. The header is: quiet caps title, an optional count
   badge, then an optional action pinned right ("All stages", "+ Add risk"). Lives here rather than in
   one component's <style> because four components need it. */
.rs-wtitle { display: flex; align-items: center; gap: .4rem; flex: 1 1 auto; min-width: 0; }
/* Truncates rather than pushing the header's right-hand action out of the card.
   This used to be 10px grey caps — SMALLER than, and the same colour as, the field labels in the column
   beside it, so a card's heading read as quieter than the data it introduced. It's on the panel step of
   the type scale now: the same voice as a "Details" section heading, one notch down. The titles are
   authored in sentence case ("Risk register"), which is also what keeps them translatable — a caps
   transform mangles languages that case differently. */
.rs-wname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--ci-ink-2); font-size: var(--ci-fs-panel); font-weight: var(--ci-fw-panel);
    letter-spacing: var(--ci-ls-panel); }
/* Count badge — the number of things in the widget. Red-ish because on these widgets it's a "needs attention" count. */
.rs-wcount { flex: none; background: var(--ci-danger-surface); color: var(--ci-danger); border-radius: var(--ci-radius-pill); padding: .02rem .38rem;
    font-size: .66rem; font-weight: 700; line-height: 1.5; }
.rs-wcount.is-quiet { background: var(--ci-hover); color: var(--ci-ink-quiet); }
/* A link/button pinned to the right of a widget header. */
.rs-wlink { flex: none; border: 0; background: transparent; color: var(--ci-accent-ink); font: inherit;
    font-size: .72rem; font-weight: 700; cursor: pointer; padding: 0; text-decoration: none; }
.rs-wlink:hover { text-decoration: underline; }
.rs-wbtn { flex: none; display: inline-flex; align-items: center; gap: .2rem; border: 1px solid color-mix(in srgb, var(--ci-accent) 22%, var(--ci-surface));
    border-radius: var(--ci-radius-sm); background: var(--ci-surface); color: var(--ci-accent-ink); font: inherit; font-size: .7rem;
    font-weight: 700; padding: .15rem .45rem; cursor: pointer; }
.rs-wbtn:hover { background: color-mix(in srgb, var(--ci-accent) 8%, var(--ci-surface)); border-color: var(--ci-accent); }
/* "Nothing here yet" inside a widget. Shared because every panel had written its own: the stage card said it
   at .78rem/#607d8b and the risk card at .84rem/#90a4ae, so two sentences doing the identical job in the same
   column looked like they came from different screens. One class, one voice — quieter than the content it
   stands in for, never so faint it reads as disabled. */
.rs-empty { margin: 0; color: var(--ci-ink-quiet); font-size: .8rem; line-height: 1.45; }
/* A row of "N of this kind" pills — Notices & Alerts' Record / Budget / Workflow / Task grid. Two up, so
   four kinds form a block you take in at once instead of a four-item list. */
.rs-pills { display: grid; grid-template-columns: 1fr 1fr; gap: .4rem; }
.rs-pill { display: flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, var(--ci-accent) 22%, var(--ci-surface)); border-radius: var(--ci-radius);
    background: var(--ci-surface); padding: .35rem .5rem; color: var(--ci-brand-ink); font-size: .78rem; font-weight: 600;
    font-family: inherit; cursor: pointer; text-align: left; }
.rs-pill:hover:not(:disabled) { background: color-mix(in srgb, var(--ci-accent) 8%, var(--ci-surface)); border-color: var(--ci-accent); }
.rs-pill .ci-icon { flex: none; color: var(--ci-accent-ink); }
.rs-pill-lbl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The pill's own count: filled when there IS something, hollow and grey when there is nothing — so an
   empty kind stays visible (you learn it was checked) without pulling the eye. */
.rs-pill-n { flex: none; min-width: 1.15rem; text-align: center; background: var(--ci-accent); color: var(--ci-ink-on-brand);
    border-radius: var(--ci-radius-sm); padding: .05rem .3rem; font-size: .7rem; font-weight: 700; }
.rs-pill.is-empty { border-color: var(--ci-line); color: var(--ci-ink-faint); background: var(--ci-surface); cursor: default; }
.rs-pill.is-empty .ci-icon { color: var(--ci-ink-disabled); }
.rs-pill.is-empty .rs-pill-n { background: var(--ci-hover); color: var(--ci-ink-disabled); }
/* A count with nothing to do: every notice is this kind, so filtering to it would change nothing. Looks like a
   live count, behaves like a label - no hand, no hover (bug 47193). */
.rs-pill.is-static { cursor: default; }
.rs-pill.is-static:hover { background: var(--ci-surface); border-color: color-mix(in srgb, var(--ci-accent) 22%, var(--ci-surface)); }
/* A dot-led row: status history entries and risk-register rows. The dot carries the state colour. */
.rs-drow { display: flex; align-items: flex-start; gap: .45rem; padding: .35rem 0; }
.rs-drow + .rs-drow { border-top: 1px solid var(--ci-line-faint); }
.rs-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ci-accent); margin-top: .4rem; }
.rs-dot-warn { background: #e8913a; }
.rs-dot-bad { background: #d64535; }
.rs-dot-ok { background: #3f9d5a; }
.rs-dbody { flex: 1 1 auto; min-width: 0; }
.rs-dtitle { color: var(--ci-accent-ink); font-size: .82rem; font-weight: 700; overflow-wrap: anywhere; }
.rs-dtitle-plain { color: var(--ci-brand-ink); }
.rs-dsub { color: var(--ci-ink-support); font-size: .74rem; line-height: 1.35; }
.rs-dmeta { color: var(--ci-ink-faint); font-size: .72rem; margin-top: .1rem; }
.rs-dside { flex: none; font-size: .74rem; font-weight: 700; }

/* Title-line badges (CiPageHeader TitleBadges): a record's state and its type/id, sized like the count pill so the
   whole line reads as one row of chips rather than three competing sizes. */
.ci-phead-badges { display: inline-flex; align-items: center; align-self: center; gap: .35rem; flex-wrap: wrap;
    position: relative; top: 1px; }
/* A badge ON the title line is sized in em, i.e. from the TITLE, while .ci-badge everywhere else stays in rem.
   That is the whole fix: at rem the chip was a fixed size sitting beside a heading that scales, so the two were
   never actually related — it read as a small object parked next to the title rather than part of it, and the
   mismatch got worse every time the title size changed or the compact tier kicked in.
   In em it tracks the heading automatically: the box lands near the title's cap height, so the chip and the words
   share a visual line. Only the header instance is touched — badges in grids and cards keep their own scale. */
.ci-phead-badges .ci-badge { font-size: .46em; padding: .55em .8em; letter-spacing: .06em; }
/* Title-line actions (CiPageHeader TitleActions) — the rename pencil. Sized in em like the badges above so it tracks
   the heading instead of being a fixed-size object parked beside it, and quiet until hovered: it sits next to the
   name permanently, so at full contrast it would compete with the name it edits. */
.ci-phead-titleactions { display: inline-flex; align-items: center; align-self: center; gap: .2rem;
    position: relative; top: 1px; }
.ci-phead-titleactions .ci-titlebtn { display: inline-grid; place-items: center; width: 1.5em; height: 1.5em;
    padding: 0; border: 0; border-radius: var(--ci-radius-sm); background: transparent; color: var(--ci-ink-faint); cursor: pointer;
    transition: background var(--ci-transition), color var(--ci-transition); }
.ci-phead-titleactions .ci-titlebtn:hover:not(:disabled) { background: var(--ci-surface, var(--ci-surface-sunk)); color: var(--ci-accent-ink); }
.ci-phead-titleactions .ci-titlebtn:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 1px; }
.ci-phead-titleactions .ci-titlebtn:disabled { opacity: .45; cursor: not-allowed; }
.ci-phead-titleactions .ci-titlebtn.on { background: var(--ci-accent); color: var(--ci-ink-on-brand); }

/* The shared badge. Neutral by default; the -ok / -warn / -off modifiers carry a STATE, so a status reads at a glance
   without anyone having to learn a colour per record type. */
.ci-badge { display: inline-flex; align-items: center; gap: .25rem; font-size: .68rem; font-weight: 700;
    letter-spacing: .05em; text-transform: uppercase; line-height: 1; white-space: nowrap;
    padding: .25rem .55rem; border-radius: var(--ci-radius-pill); border: 1px solid var(--ci-line-strong); background: var(--ci-surface-sunk); color: var(--ci-ink-2); }
.ci-badge-ok { background: var(--ci-surface-sunk); border-color: var(--ci-line-strong); color: var(--ci-ok); }
.ci-badge-warn { background: var(--ci-warn-surface); border-color: var(--ci-line); color: var(--ci-warn); }
.ci-badge-off { background: var(--ci-surface-sunk); border-color: var(--ci-line-strong); color: var(--ci-ink-quiet); }
/* The fourth state the set was missing: something that needs attention rather than merely being warned about
   (an urgent request, a failed run). Added when the accordions were unified — System Requests had a private red
   pill, and reaching for `.ci-badge-warn` for "Urgent" would have said the wrong thing. */
.ci-badge-alert { background: var(--ci-surface-sunk); border-color: var(--ci-line-strong); color: var(--ci-danger); }

/* A MERGE PLACEHOLDER, and the copy button for it — the two are the same control (`CiToken`). Every screen that shows
   a `{---Field---}` token uses this: template workbench, mergeable clauses, notification fields, and the example
   tokens inside hint sentences. It replaced six near-identical private `.tw-token-copy` rules.

   Sized in `em` throughout, so one class fits a 12px grid cell and a body-copy sentence with no size variant.

   The resting state is a bare tinted chip with NO border and an invisible glyph — a token repeats a few dozen times
   down a fields tab, and painting a bordered box around each one puts chrome in front of the data (U12: chrome that
   repeats per row is a ghost). The border and the glyph arrive on hover, which is also where the "click to copy"
   message lands. */
.ci-tok { display: inline-flex; align-items: center; gap: .3em; max-width: 100%; vertical-align: baseline;
    padding: .05em .35em; border: 1px solid transparent; border-radius: var(--ci-radius-xs);
    background: var(--ci-hover); color: var(--ci-accent-ink); font-size: .78em; line-height: 1.5;
    cursor: pointer; text-align: left; transition: border-color var(--ci-transition), background var(--ci-transition);
    /* The "Copied" confirmation is positioned against this box. */
    position: relative; }
.ci-tok > code { font-family: inherit; font-size: 1em; color: inherit;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ci-tok > .ci-icon { flex: none; color: var(--ci-ink-faint); opacity: 0; transition: opacity var(--ci-transition); }
.ci-tok:hover { border-color: var(--ci-line-strong); }
.ci-tok:hover > .ci-icon { opacity: 1; color: var(--ci-accent-ink); }
.ci-tok:focus-visible { outline: none; border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
/* A keyboard user gets the glyph too — focus is the same "you are on it" state as hover. */
.ci-tok:focus-visible > .ci-icon { opacity: 1; color: var(--ci-accent-ink); }

/* The confirmation OVERLAYS the token instead of replacing its text, so the chip keeps its exact measured size and a
   tab holding thirty tokens never reflows when one is clicked. It is deliberately not a toast: someone copying eight
   placeholders in a row would get eight toasts, and the answer to "did that work?" belongs on the thing clicked. */
.ci-tok-done { position: absolute; inset: 0; display: inline-flex; align-items: center; justify-content: center;
    gap: .25em; border-radius: inherit; font-weight: 600; white-space: nowrap;
    background: var(--ci-ok-surface); color: var(--ci-ok);
    animation: ci-veil-in var(--ci-enter) var(--ci-enter-ease) both; }
.ci-tok-done .ci-icon { flex: none; color: currentColor; opacity: 1; }
.ci-tok.is-copied { border-color: var(--ci-ok-line); }

/* IconOnly: the glyph IS the control, so unlike the full chip it has to be visible at rest — quiet, but there. Used
   where the row already names the placeholder in words (a stage card) and the token would be the widest thing on it. */
.ci-tok-mini { padding: .05em .25em; background: transparent; }
.ci-tok-mini > .ci-icon { opacity: .55; }
.ci-tok-mini:hover > .ci-icon, .ci-tok-mini:focus-visible > .ci-icon { opacity: 1; }
/* Rendered model reply (ChatMarkdown.Render output). Every VISDOM surface shows the same kind of answer — bold runs,
   short numbered steps, the occasional link — so the spacing that makes those readable belongs here rather than being
   re-derived per panel. Scoped tight: only the tags the renderer itself emits. */
.ci-md { white-space: normal; }
.ci-md p { margin: 0 0 .5rem; }
.ci-md p:last-child { margin-bottom: 0; }
.ci-md ul, .ci-md ol { margin: .25rem 0 .5rem; padding-left: 1.25rem; }
.ci-md ul:last-child, .ci-md ol:last-child { margin-bottom: 0; }
.ci-md li { margin: .12rem 0; }
.ci-md strong { font-weight: 700; }
.ci-md code { background: var(--ci-hover); border-radius: var(--ci-radius-xs); padding: 0 .25rem; font-size: .82em; }
.ci-md a { color: var(--ci-accent-ink); }
/* A reply's "## Heading" is a section marker inside a panel, not a page title — same size as the text, just bolder. */
.ci-md h4, .ci-md h5, .ci-md h6 { margin: .6rem 0 .25rem; font-size: 1em; font-weight: 700; color: var(--ci-brand-ink); }
.ci-md h4:first-child, .ci-md h5:first-child, .ci-md h6:first-child { margin-top: 0; }
/* Answers about a list come back as a pipe table. display:block + overflow-x keeps a wide one scrolling inside its
   own panel instead of stretching the page. */
.ci-md-table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: .3rem 0 .5rem; font-size: .85em; }
.ci-md-table th, .ci-md-table td { border: 1px solid var(--ci-line-strong); padding: .25rem .5rem; text-align: left; }
.ci-md-table th { background: var(--ci-surface-sunk); color: var(--ci-brand-ink); font-weight: 700; }

/* The :not() list is the TITLE WORDS only — every other span on the line keeps its own colour. A new title-line
   span must be excluded here too, or the gradient's transparent text-fill makes its contents invisible. */
.ci-phead--ai .ci-phead-title > span:not(.ci-phead-count):not(.ci-phead-badges):not(.ci-phead-titleactions):not(.ci-ainote-tip):not(.ci-icon) {
    color: var(--ci-ai-ink); /* fallback for engines without background-clip:text */
    background: linear-gradient(92deg, #5b21b6 0%, #7c3aed 48%, #a855f7 100%);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent;
}
.ci-phead--ai .ci-phead-count { color: var(--ci-ai-ink); background: var(--ci-ai-soft); border-color: color-mix(in srgb, var(--ci-ai) 22%, var(--ci-surface)); }

/* Canonical page-header action buttons — replaces the per-page .ci-btn / .md-btn / .rpt-* / .ci-export drift. */
.ci-hbtn { display: inline-flex; align-items: center; justify-content: center; gap: .35rem;
    height: var(--ci-btn-h); padding: 0 1rem;
    box-sizing: border-box; border: 1px solid var(--ci-line-strong); background: var(--ci-surface); color: var(--ci-brand-ink); border-radius: var(--ci-radius);
    font-size: .85rem; font-weight: 600; cursor: pointer; text-decoration: none; line-height: 1; white-space: nowrap;
    transition: background var(--ci-transition), border-color var(--ci-transition), color var(--ci-transition); }
.ci-hbtn:hover { background: var(--ci-hover); border-color: var(--ci-line-strong); }
.ci-hbtn:disabled { opacity: .5; cursor: not-allowed; }
.ci-hbtn-primary { background: var(--ci-accent); border-color: var(--ci-accent); color: var(--ci-ink-on-brand); border: none; }
.ci-hbtn-primary:hover { background: var(--ci-accent-dark); border-color: var(--ci-accent-dark); color: var(--ci-ink-on-brand); }
.ci-hbtn-danger { color: var(--ci-danger); border-color: var(--ci-danger-line); background: var(--ci-surface); }
.ci-hbtn-danger:hover { background: var(--ci-danger-surface); border-color: var(--ci-danger-line); color: var(--ci-danger); }
/* Icon-only header button — square, so it reads as a tool next to the labelled actions. Needs a title/aria-label.
   Square means width tracks the height token, or it stops being square the moment that changes. */
.ci-hbtn-icon { width: var(--ci-btn-h); padding: 0; }

/* An element the page put into native full screen (e.g. the graphical designer host). Drop the card treatment and
   fill the display — the browser sizes the element itself, but the inherited border/radius would still show. */
:fullscreen { border: 0 !important; border-radius: 0 !important; min-height: 0 !important; background: var(--ci-surface); }
.ci-hbtn-ai { background: var(--ci-ai); border-color: var(--ci-ai); color: var(--ci-ink-on-brand); }
.ci-hbtn-ai:hover { background: var(--ci-ai); border-color: var(--ci-ai); color: var(--ci-ink-on-brand); filter: brightness(1.08); }

/* ── Grid-action tier (small) ──────────────────────────────────────────────────────────
   The ONE small button for row actions in SECONDARY grids — admin, report, list, and management
   pages. Main record list grids and record sub-grids keep their own medium "Manage" row button
   (.ci-grid-action); everything else uses .ci-gbtn so row actions are one consistent size app-wide.
   Mirrors the .ci-hbtn header tier's look, just smaller (26px), so the tiers read as a family. */
.ci-gbtn { display: inline-flex; align-items: center; justify-content: center; gap: .3rem; height: 26px; padding: 0 .6rem;
           font-size: .76rem; font-weight: 600; line-height: 1; border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius-sm);
           background: var(--ci-surface); color: var(--ci-brand-ink); cursor: pointer; text-decoration: none; white-space: nowrap;
           transition: background-color var(--ci-transition), border-color var(--ci-transition), color var(--ci-transition); }
.ci-gbtn:hover { background: var(--ci-hover); border-color: var(--ci-line-strong); color: var(--ci-brand-ink); }
.ci-gbtn:disabled { opacity: .5; cursor: not-allowed; }
.ci-gbtn-primary { background: var(--ci-accent); border-color: var(--ci-accent); color: var(--ci-ink-on-brand); }
.ci-gbtn-primary:hover { background: var(--ci-accent-dark); border-color: var(--ci-accent-dark); color: var(--ci-ink-on-brand); }
/* The LEAD action in a table row (Manage / Open / Run) — the labelled one beside the icon-only actions. Outlined, not
   filled: it repeats on every row, so a fill turned the grid into a column of blue blocks. Use .ci-gbtn-primary for a
   genuine one-off primary (a sub-grid's "Add"), never for a per-row action. */
.ci-gbtn-lead { color: var(--ci-brand-ink); border-color: var(--ci-line-strong); background: var(--ci-surface); }
.ci-gbtn-lead:hover { background: var(--ci-surface-sunk); border-color: var(--ci-line-emphasis); color: var(--ci-brand-ink); }
.ci-gbtn-danger { color: var(--ci-danger); border-color: var(--ci-danger-line); background: var(--ci-surface); }
.ci-gbtn-danger:hover { background: var(--ci-danger-surface); border-color: var(--ci-danger-line); color: var(--ci-danger); }
/* Predates .ci-gbtn-icon being ghost by default, and is now REDUNDANT alongside it — all ten call sites are
   `ci-gbtn ci-gbtn-icon ci-gbtn-ghost`. Kept (rather than removed from ten files) but its hover is aligned: it
   used to draw a border on hover, which would have made those ten buttons the only ghosts in the app whose
   hover has an outline. Don't add it to new markup; .ci-gbtn-icon alone is the ghost icon button. */
.ci-gbtn-ghost { border-color: transparent; background: transparent; }
.ci-gbtn-ghost:hover { background: var(--ci-hover-strong); border-color: transparent; }
/* ── Compact tier (22px) — a card header's own action ─────────────────────────────────
   The fourth and smallest LABELLED tier, under .ci-gbtn's 26px. Its home is the header strip of a card or panel,
   where the button sits beside a title and a count badge and is chrome rather than an action on a row of data:
   at 26px it stood a head taller than the text it was paired with and the strip read as two misaligned things.
   Anything sized to a GRID ROW stays .ci-gbtn — this tier is not a smaller row action, it is a different job.
   Same look as the tiers above it, so the four read as one family; only the height and the padding come down.
   ★ Desktop chrome only. 22px is below the 24px touch-target floor, so never put this on a touch surface or on
   the primary way through a task; a card header's "Manage", beside a label that already names the thing, is
   exactly the case it was cut for. Pair it with .ci-cbtn-n for a badge that sits level with it. */
.ci-cbtn { display: inline-flex; align-items: center; justify-content: center; gap: .25rem;
           height: var(--ci-btn-h-xs); box-sizing: border-box; padding: 0 .5rem;
           font-size: .72rem; font-weight: 600; line-height: 1; border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius-sm);
           background: var(--ci-surface); color: var(--ci-brand-ink); cursor: pointer; text-decoration: none; white-space: nowrap;
           transition: background-color var(--ci-transition), border-color var(--ci-transition), color var(--ci-transition); }
.ci-cbtn:hover { background: var(--ci-hover); border-color: var(--ci-line-strong); color: var(--ci-brand-ink); }
.ci-cbtn:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 1px; }
.ci-cbtn:disabled { opacity: .5; cursor: not-allowed; }
/* The count/status pill that pairs with it. Its own height, not its padding's — left to pad itself it came out
   ~15px against a 26px button and sat 5px low (measured on the doc-template cards, 2026-09-30). */
.ci-cbtn-n { display: inline-flex; align-items: center; height: var(--ci-btn-h-xs); box-sizing: border-box;
             padding: 0 .5rem; background: var(--ci-surface); border: 1px solid var(--ci-line-strong);
             color: var(--ci-ink-support); border-radius: var(--ci-radius-pill);
             font-size: .7rem; font-weight: 700; white-space: nowrap; }

/* ── Icon-only small button (secondary row actions in sub-grids / bespoke tables) ─────────────────────────────
   Square, no label, tooltip carries the name — and GHOST, for the same reason .ci-grid-tool is (see CiDataGrid):
   this is chrome multiplied by the row count, so an outline here paints a wall of empty rounded boxes in front of
   the data. The glyph is the button; the box appears on hover.
   The 26px is kept at rest, so the strip's width and the column's alignment do not move — a paint change, not a
   layout one. A LABELLED .ci-gbtn keeps its outline: one outlined lead action per row is what makes the primary
   findable, and it was the repetition of the secondaries that was the noise.
   Disabled stays visible and greyed rather than growing its box back — an unavailable action must not become the
   most prominent thing in a column of borderless ones (rule 12: disable with a reason, never hide). */
/* flex: none — an icon button must never be squeezed NARROWER than its glyph. `width: 26px` is only a flex BASIS,
   so in a cramped action strip these were rendering 15-22px wide and clipping the icon (measured across three
   record tabs). Pre-dates the ghost change; it was just easier to see once the box stopped drawing a border. */
.ci-gbtn-icon { width: 26px; flex: none; padding: 0; border-color: transparent; background: transparent; color: var(--ci-ink-support); }
.ci-gbtn-icon:hover { background: var(--ci-hover-strong); border-color: transparent; color: var(--ci-brand-ink); }
.ci-gbtn-icon:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: -2px; }
.ci-gbtn-icon:disabled { opacity: .4; background: transparent; border-color: transparent; }
.ci-gbtn-icon:disabled:hover { background: transparent; }
/* A destructive icon-only action keeps its red glyph but loses the pink outline; the wash arrives on hover. */
.ci-gbtn-icon.ci-gbtn-danger { border-color: transparent; background: transparent; color: var(--ci-danger); }
.ci-gbtn-icon.ci-gbtn-danger:hover { background: var(--ci-danger-surface); border-color: transparent; color: var(--ci-danger); }

/* ── Medium button tier (30px) — the canonical definition ──────────────────────────────
   The in-body tier: panel/sub-grid/toolbar buttons that sit between the 36px page-header .ci-hbtn and the 26px
   row-action .ci-gbtn. It lived duplicated in ~37 page <style> blocks, which is how it drifted (28 vs 30px, different
   greys) — and 11 shared components (GatedButton, CiConfirmDialog, LinkPicker, …) used the class with NO definition at
   all, so their buttons rendered as bare browser default. A page <style> block still overrides this by cascade order,
   so hoisting it changes nothing for the pages that define their own. New pages should use it as-is. */
.ci-btn { display: inline-flex; align-items: center; gap: .25rem; height: var(--ci-btn-h-sm); box-sizing: border-box;
    background: var(--ci-accent); color: var(--ci-ink-on-brand) !important; border: none; text-decoration: none; padding: 0 .65rem;
    border-radius: var(--ci-radius-sm); font-size: .8rem; font-weight: 600; line-height: 1; cursor: pointer; white-space: nowrap; }
.ci-btn:hover { background: var(--ci-accent-dark); }
/* not-allowed, matching .ci-hbtn and .ci-gbtn (pointer audit, 2026-09-06). The three tiers used to disagree: the
   header and row tiers said not-allowed while the in-body tier — the most common button in the app — said default,
   so the same disabled state answered "can I press this?" two different ways depending on the button's size. The
   pointer is the FIRST answer to that question, before the tooltip appears and before anyone reads grey text, and
   rule 10 (disable with a reason, never hide) makes disabled controls ubiquitous here. */
.ci-btn:disabled { opacity: .55; cursor: not-allowed; }
.ci-btn-danger { background: #b3261e; }
.ci-btn-danger:hover { background: #8f1e18; }

/* The page-level failure panel (MainLayout's ErrorBoundary). Deliberately calm and small: what failed is one
   screen, not the application, and the shell around this is still working. */
.ci-pagefail { max-width: 34rem; margin: 3rem auto; background: var(--ci-surface); border: 1px solid var(--ci-line); border-radius: var(--ci-radius-lg);
    padding: 1.4rem 1.5rem; text-align: center; box-shadow: 0 1px 3px rgba(20, 45, 80, .08); }
.ci-pagefail-title { font-size: 1rem; font-weight: 700; color: var(--ci-brand-ink); margin-bottom: .4rem; }
.ci-pagefail-text { font-size: .84rem; color: var(--ci-ink-2); line-height: 1.55; margin: 0 0 1rem; }
/* The fault's reference code. Quiet, but monospaced and letter-spaced because its whole job is to be read aloud
   accurately down a phone - support then searches it on the hidden Developer tools page to get the stack. */
.ci-pagefail-ref { font-size: .78rem; color: var(--ci-ink-quiet); margin: 0 0 1rem; }
.ci-pagefail-ref strong { font-family: var(--ci-font-mono, ui-monospace, Consolas, monospace);
                          font-size: .86rem; letter-spacing: .08em; color: var(--ci-ink-2); }
.ci-pagefail-acts { display: flex; gap: .5rem; justify-content: center; }
/* The administrator-only "what this reference means" disclosure. Left-aligned inside a centred card on purpose:
   it is reference material to read, not a message to take in at a glance, and centred key/value pairs are unreadable.
   Sunk rather than bordered so it reads as a note on the card, not a second card. */
.ci-pagefail-why { margin: 0 0 1rem; text-align: left; background: var(--ci-surface-sunk); border: 1px solid var(--ci-line);
    border-radius: var(--ci-radius-sm); padding: .5rem .7rem; }
.ci-pagefail-why > summary { cursor: pointer; font-size: .78rem; font-weight: 600; color: var(--ci-ink-2); list-style: none; }
.ci-pagefail-why > summary::-webkit-details-marker { display: none; }
/* A rotating caret, so open/closed is legible without the native marker the summary above switches off. */
.ci-pagefail-why > summary::before { content: "\25B8"; display: inline-block; width: .8em; color: var(--ci-ink-quiet);
    transition: transform .12s ease; }
.ci-pagefail-why[open] > summary::before { transform: rotate(90deg); }
.ci-pagefail-whybody { display: grid; grid-template-columns: auto 1fr; gap: .2rem .6rem; margin: .6rem 0 .5rem; font-size: .76rem; }
.ci-pagefail-whybody dt { color: var(--ci-ink-quiet); font-weight: 600; }
.ci-pagefail-whybody dd { margin: 0; color: var(--ci-ink-2); min-width: 0; }
/* The frame can be a very long single token with no spaces to break on, and this card is only 34rem wide. */
.ci-pagefail-whybody dd code { font-family: var(--ci-font-mono, ui-monospace, Consolas, monospace); font-size: .72rem;
    overflow-wrap: anywhere; }
.ci-pagefail-whynote { margin: .5rem 0 0; font-size: .7rem; color: var(--ci-ink-quiet); line-height: 1.45; }
.ci-btn-secondary { display: inline-flex; align-items: center; gap: .25rem; height: var(--ci-btn-h-sm); box-sizing: border-box;
    background: var(--ci-surface); color: var(--ci-brand-ink) !important; border: 1px solid var(--ci-line-strong); text-decoration: none;
    padding: 0 .6rem; border-radius: var(--ci-radius-sm); font-size: .8rem; font-weight: 600; line-height: 1; cursor: pointer;
    white-space: nowrap; }
.ci-btn-secondary:hover { background: var(--ci-hover); border-color: var(--ci-ink-disabled); }
.ci-btn-secondary:disabled { opacity: .5; cursor: not-allowed; }
/* A secondary button acting as a toggle that is currently ON (e.g. an Approval/Signature segmented pair). */
.ci-btn-secondary.is-on { background: var(--ci-surface-sunk); border-color: var(--ci-accent); color: var(--ci-brand-ink) !important; }

/* A FILE PICKER that looks like the rest of the app. A bare <input type="file"> / <InputFile> renders the BROWSER's
   own widget — a grey system button and "No file chosen" in the system font — so it is the one control on a screen
   that ignores the theme entirely, and beside a .ci-btn it reads as unfinished markup. Styling it via
   ::file-selector-button only reaches the button half; the "No file chosen" text and its truncation stay the
   browser's. So: wrap the input in a <label class="ci-filepick"> holding a .ci-filepick-btn span and a
   .ci-filepick-name span carrying the chosen file's name from the model. The real input stays in the DOM and stays
   focusable — Blazor's InputFile streaming is untouched — it is only visually hidden, and the <label> makes the
   whole strip the click target. Sized off --ci-btn-h-sm so it lines up with the .ci-btn beside it (U7). */
.ci-filepick { position: relative; display: inline-flex; align-items: center; gap: .5rem; max-width: 22rem; cursor: pointer; }
.ci-filepick > input[type=file] { position: absolute; inset: 0 auto auto 0; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.ci-filepick-btn { display: inline-flex; align-items: center; flex: 0 0 auto; height: var(--ci-btn-h-sm); box-sizing: border-box;
    background: var(--ci-surface); color: var(--ci-brand-ink); border: 1px solid var(--ci-line-strong);
    padding: 0 .6rem; border-radius: var(--ci-radius-sm); font-size: .8rem; font-weight: 600; line-height: 1; white-space: nowrap; }
.ci-filepick:hover .ci-filepick-btn { background: var(--ci-hover); border-color: var(--ci-ink-disabled); }
.ci-filepick:focus-within .ci-filepick-btn { border-color: var(--ci-accent); }
.ci-filepick-name { min-width: 0; font-size: .8rem; color: var(--ci-ink-support); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Nothing chosen yet: the placeholder rung, so a real filename is clearly the louder of the two states. */
.ci-filepick-name.is-empty { color: var(--ci-ink-quiet); font-weight: 400; }
.ci-filepick.is-disabled { cursor: default; opacity: .55; }

/* TIER COLLISION: GatedButton always emits "ci-btn" plus whatever CssClass it is given, so using it in a page header
   ("ci-hbtn") or a grid row ("ci-gbtn") produces both classes on one element — and .ci-btn, being later in this file,
   would otherwise win and render a 36px header action as a 30px accent-filled one. These re-assert the intended tier
   whenever the classes are combined, so a permission-gated action can live in a header or a row without looking wrong. */
.ci-btn.ci-hbtn { height: var(--ci-btn-h); padding: 0 1rem; border: 1px solid var(--ci-line-strong); background: var(--ci-surface); color: var(--ci-brand-ink) !important;
    border-radius: var(--ci-radius); font-size: .85rem; gap: .35rem; justify-content: center; }
/* Same collision, same fix, for a permission-gated SECONDARY action — a gated button asked to be secondary would
   otherwise come out accent-filled with white-on-white text (`.ci-btn` sets `color: #fff !important`), which reads as
   an empty blue box. Needed by any card or panel whose actions are one primary plus quieter siblings. */
.ci-btn.ci-btn-secondary { background: var(--ci-surface); color: var(--ci-brand-ink) !important; border: 1px solid var(--ci-line-strong); }
.ci-btn.ci-btn-secondary:hover { background: var(--ci-hover); border-color: var(--ci-ink-disabled); }
.ci-btn.ci-hbtn:hover { background: var(--ci-hover); border-color: var(--ci-line-strong); }
/* --ci-ink-on-brand, NOT --ci-ink-disabled: `.ci-btn.ci-hbtn` above wins the cascade on colour, so a gated PRIMARY
   needs the white re-asserted here or its label comes out the disabled grey on a saturated blue (~2.9:1). */
.ci-btn.ci-hbtn.ci-hbtn-primary { background: var(--ci-accent); border-color: var(--ci-accent); color: var(--ci-ink-on-brand) !important; }
.ci-btn.ci-hbtn.ci-hbtn-primary:hover { background: var(--ci-accent-dark); border-color: var(--ci-accent-dark); }
.ci-btn.ci-hbtn.ci-hbtn-danger { color: var(--ci-danger) !important; border-color: var(--ci-danger-line); background: var(--ci-surface); }
.ci-btn.ci-hbtn.ci-hbtn-danger:hover { background: var(--ci-danger-surface); }
.ci-btn.ci-gbtn { height: 26px; padding: 0 .6rem; font-size: .76rem; border: 1px solid var(--ci-line-strong); background: var(--ci-surface);
    color: var(--ci-brand-ink) !important; border-radius: var(--ci-radius-sm); gap: .3rem; }
.ci-btn.ci-gbtn:hover { background: var(--ci-hover); border-color: var(--ci-line-strong); }

/* ── DRAG TO REORDER: THE HANDLE AND THE TWO ROW STATES ──────────────────────────────────────────────────────
   The one look for reordering an ordered table, shared by every screen that has one (CiReorderGrip + RowReorder).
   It lived as a private `.xx-grip` in eight page style blocks before this, and had already drifted on colour,
   size and whether the drop line was a border or a shadow — so two screens doing the same job didn't look like
   it (U2/U10). The handle is a real button, so it needs the button reset as well as the grip look.

   The drop line is an inset box-shadow rather than a border-top: a border changes the row's height, so every row
   the pointer crosses nudges the ones below it and the list twitches under the drag. */
.ci-grip { display: inline-flex; align-items: center; justify-content: center; width: 1.15rem; height: 1.35rem;
    padding: 0; border: 0; background: none; color: var(--ci-ink-disabled); font-size: .9rem; line-height: 1; cursor: grab;
    border-radius: var(--ci-radius-xs); vertical-align: middle; }
.ci-grip:hover:not(:disabled) { color: var(--ci-accent-ink); background: color-mix(in srgb, var(--ci-accent) 8%, var(--ci-surface)); }
/* Keyboard reorder is the handle's second job (arrow keys), so the focus ring has to be visible — it is the only
   thing telling someone not using a mouse which row the arrow keys will move. */
.ci-grip:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 1px; color: var(--ci-accent-ink); }
.ci-grip:active:not(:disabled) { cursor: grabbing; }
.ci-grip:disabled { color: var(--ci-ink-disabled); cursor: default; }
tr.ci-dragging, .ci-dragging { opacity: .4; }
tr.ci-dropping, .ci-dropping { box-shadow: inset 0 2px 0 var(--ci-accent); }

/* THE SAME HANDLE ON A CiDataGrid. A grid that opts into drag-to-reorder gets Syncfusion's own handle cell, which
   carries a grab cursor and nothing to look at — so the two kinds of orderable list didn't advertise themselves the
   same way, and on the grid you had to discover the feature by hovering. This paints the identical glyph into that
   cell. It lives here rather than in CiDataGrid's own style block because it is one half of a pair with .ci-grip
   directly above, and the two drifting apart is the whole thing this section exists to prevent. */
.ci-grid-reorder .e-grid td.e-rowdragdrop::before,
.ci-grid-reorder .e-grid .e-rowdragdropcell::before {
    content: "\283F"; color: var(--ci-ink-disabled); font-size: .9rem; line-height: 1; }
/* CENTRE THE GLYPH IN ITS COLUMN. The handle is drawn as this cell's ::before, so where it lands is decided by the
   cell's own text-align and padding — and Syncfusion gives every body cell a left inset for reading text. That put
   the handle hard against the left edge of a column whose only content is the handle, and the wider the grid let
   that column get, the further it drifted from the "#" centred above it. Centre both halves and drop the inset. */
.ci-grid-reorder .e-grid td.e-rowdragdrop,
.ci-grid-reorder .e-grid .e-rowdragdropcell {
    text-align: center; padding-left: 0; padding-right: 0; }
.ci-grid-reorder .e-grid .e-row:hover td.e-rowdragdrop::before,
.ci-grid-reorder .e-grid .e-row:hover .e-rowdragdropcell::before { color: var(--ci-accent-ink); }
/* Syncfusion puts its own drag icon span in the cell; ours is the glyph, so hide theirs rather than show both. */
.ci-grid-reorder .e-grid .e-icon-rowdragicon::before { content: none; }
/* "#" over the handle column. The handle replaced the position-number column on these grids — a column of handles
   already says the rows are in a deliberate order — so this one character is what carries "this is the order". */
.ci-grid-reorder .e-grid .e-headercell.e-rowdragheader::before,
.ci-grid-reorder .e-grid th.e-rowdragheader::before {
    content: "#"; color: rgba(255, 255, 255, .75); font-size: .78rem; font-weight: 600; }
.ci-grid-reorder .e-grid .e-headercell.e-rowdragheader,
.ci-grid-reorder .e-grid th.e-rowdragheader {
    text-align: center; padding-left: 0; padding-right: 0; }

/* ── ONE PRIMARY PER FOOTER, AND IT SITS AT THE RIGHT ────────────────────────────────────────────────────────
   Every drawer and dialog footer follows the same shape: the quieter choices at the left, the commit action alone
   at the right edge. It is enforced here rather than asked of each page, because asking did not work — roughly
   half the drawers in the app were declaring Cancel as a bare `.ci-btn`, which IS the app's filled accent button,
   so "Cancel  Save" rendered as two blue blocks with nothing to look at first.

   Two rules do it, whatever order a page writes its buttons in:
     * a bare .ci-btn in a footer paints as a secondary (the primary must ask for .ci-btn-primary);
     * the primary is ordered last and pushed right.
   A page that genuinely needs something at the far left (a count, a hint) keeps using .ci-*-foot-start. */
.ci-drawer-foot > .ci-btn:not(.ci-btn-primary):not(.ci-btn-danger),
.ci-dlg-foot > .ci-btn:not(.ci-btn-primary):not(.ci-btn-danger) {
    background: var(--ci-surface); color: var(--ci-brand-ink) !important; border: 1px solid var(--ci-line-strong); }
.ci-drawer-foot > .ci-btn:not(.ci-btn-primary):not(.ci-btn-danger):hover,
.ci-dlg-foot > .ci-btn:not(.ci-btn-primary):not(.ci-btn-danger):hover {
    background: var(--ci-hover); border-color: var(--ci-ink-disabled); }
/* The buttons stay ONE right-aligned group — Cancel does not get stranded at the far left — and the primary is
   ordered last within it, whatever order the page declared them in. */
.ci-drawer-foot > *, .ci-dlg-foot > * { order: 1; }
.ci-drawer-foot > .ci-btn-primary, .ci-dlg-foot > .ci-btn-primary { order: 2; }
.ci-drawer-foot > .ci-drawer-foot-start, .ci-dlg-foot > .ci-dlg-foot-start { order: 0; }

/* ── THE SAME SHAPE FOR AN IN-BODY ACTION ROW, BUT ON THE LEFT ────────────────────────────────────────────────
   `.ci-actions` is the in-body counterpart of the two footers above: the action row that sits under a panel or a
   tab's content ("Save rules" + "Count matching records", "Choose which lists are governed", a pair of go-there
   links).

   It shares the footers' ORDER and their painting, but not their alignment — 2026-08-20, replacing an earlier
   right-aligned version of this block. A header and a dialog footer are chrome: they belong to the frame, and the
   frame's right edge is where the eye has learned to find them. An in-body row is not chrome — it belongs to the
   controls directly above it, and it starts where they start. Right-aligning it under a stack of left-aligned
   fields floats the commit away from the form it commits, and the app is overwhelmingly left-aligned in the body,
   so the right-aligned rows were the odd ones out rather than the standard.

   Rules, whatever order a page declares its buttons in:
     * the row is LEFT-aligned, on the page's own left margin;
     * a bare .ci-btn paints as a SECONDARY — the primary must ask for .ci-btn-primary;
     * the primary is still ordered LAST — rightmost WITHIN the group. That part doesn't change with alignment:
       the commit is the last button in every row in the app, so it is always found in the same place relative to
       its neighbours;
     * status text (a count, a caveat) goes in .ci-actions-start and is pushed to the far RIGHT, away from the
       buttons, where it reads as an answer instead of as another control. */
.ci-actions { display: flex; align-items: center; justify-content: flex-start; gap: .6rem; flex-wrap: wrap;
              margin-top: .8rem; }
.ci-actions > .ci-btn:not(.ci-btn-primary):not(.ci-btn-danger) {
    background: var(--ci-surface); color: var(--ci-brand-ink) !important; border: 1px solid var(--ci-line-strong); }
.ci-actions > .ci-btn:not(.ci-btn-primary):not(.ci-btn-danger):hover {
    background: var(--ci-hover); border-color: var(--ci-ink-disabled); }
.ci-actions > * { order: 1; }
.ci-actions > .ci-btn-primary { order: 2; }
/* Ordered after the primary and pushed over, so it sits at the far edge rather than crowding the last button. */
.ci-actions > .ci-actions-start { order: 3; margin-left: auto; color: var(--ci-ink-support); font-size: .84rem; }

/* ── THE EDIT BAR — A MODE THAT DROPS INTO THE PAGE ───────────────────────────────────────────────────────────
   `.ci-editbar` is the panel that appears when a page enters an editing/composing MODE and the thing being edited
   stays visible and directly manipulable underneath it — the dashboard composer's arranging panel, the "Add
   dashboard" entry row. It is NOT a drawer or a dialog (those cover the page and have their own scrim and footer
   classes) and it is NOT `.ci-actions` (an action row belonging to a form). It is the third case: page-level chrome
   that arrives, hangs around while you work below it, and leaves.

   It is shared rather than private because the first one was private, and a private one is how the second page gets
   a panel that is nearly the same and subtly isn't. If a page needs a variation, add a modifier here.

   THE POINT OF IT is that it must not look like the page it edits. Three things do that, and all three are load-
   bearing rather than decorative:
     * an accent TOP EDGE and a faint accent tint — the page's own cards and bars are plain surface on plain ground;
     * a real SHADOW, which U12 otherwise reserves for something floating over the page — honest here, because this
       panel really does float over the content once it pins;
     * a MODE LABEL (`.ci-editbar-mode`), so the dashed outlines and ✕ buttons on the surface below read as an
       invitation rather than as a fault.

   AND IT PINS. `position: sticky` keeps the panel — and therefore its commit button — on screen however far down
   the page you have scrolled. That is the whole reason it exists: the dashboard composer's Save was reported as
   "I couldn't find the save button" when it was a flat strip at the top of a long scrolling page.

   TWO GOTCHAS, both found the hard way:
     * `top: 0` pins to the top of the SCROLLER'S PADDING BOX, so page content is visible in that band as it slides
       past. The `::before` is the same opaque shield `.ci-tabs-wrap` uses on a record page, sized from the same
       `--ci-scroll-pad` token — the band it has to mask — and never from a literal. That drift is what kept shaving
       the record title's descenders.
     * the z-index is NOT a small number by accident. `SfDashboardLayout` writes `z-index: 1000` INLINE on every
       panel it lays out, at rest and not only while dragging, so at any sane local value the pinned bar was visible
       at the top of the page and slid silently under the widgets the moment you scrolled — the one state it exists
       for. `--ci-z-float` is the app's rung for a persistent on-screen floating affordance and is deliberately
       below the nav rail, anchored popovers and a modal's scrim, all of which must still cover it. */
.ci-editbar { position: sticky; top: 0; z-index: var(--ci-z-float); display: flex; flex-direction: column; gap: .5rem;
    background: color-mix(in srgb, var(--ci-accent) 5%, var(--ci-surface));
    border: 1px solid var(--ci-line); border-top: 3px solid var(--ci-accent);
    border-radius: var(--ci-radius); box-shadow: var(--ci-shadow-lg); padding: .55rem .7rem; }
.ci-editbar::before { content: ""; position: absolute; left: -1px; right: -1px; bottom: 100%;
    height: var(--ci-scroll-pad); background: var(--ci-content-bg); }
/* One row of the panel. A panel is usually two: what you are editing, then what you can do about it. */
.ci-editbar-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
/* What mode you are in. Accent INK (not --ci-accent, which is a fill and does not invert) — U12. */
.ci-editbar-mode { display: inline-flex; align-items: center; gap: .35rem; color: var(--ci-accent-ink);
    font-size: .85rem; font-weight: 700; white-space: nowrap; }
/* Guidance about the surface BELOW, centred in whatever the row's controls leave rather than pinned against one of
   them — pinned left it reads as a caption on the button beside it. `flex: 1` also makes the row wrap on a narrow
   window instead of letting the text collide with a button. */
.ci-editbar-hint { flex: 1 1 8rem; min-width: 0; text-align: center; color: var(--ci-ink-support); font-size: .8rem; }
/* U7: a control on this panel is the same height as the buttons beside it, taken from the token and NEVER from
   padding or a rem — this is chrome, so it is the 32px tier. Scoped to the panel's own inputs and selects so a page
   dropping a field in here gets the tier for free and cannot pad-size its way off it. */
.ci-editbar input:not([type=checkbox]):not([type=radio]), .ci-editbar select, .ci-editbar .ci-editbar-in {
    height: var(--ci-btn-h); box-sizing: border-box; padding: 0 .55rem; border: 1px solid var(--ci-line-strong);
    border-radius: var(--ci-radius-sm); font: inherit; font-size: .85rem; }

/* The row-action strip inside a BESPOKE table's first cell (an editor / nested-row table that legitimately isn't a
   CiDataGrid). Same spacing rule as the grid's own .ci-grid-acts, so a hand-built row's buttons sit exactly like a
   grid row's — pages must not hand-roll their own .xx-rowacts with a different gap. */
.ci-rowacts { display: inline-flex; align-items: center; justify-content: flex-start; gap: .4rem; white-space: nowrap; }

/* ── Record-dock content surface (toolbar · panel · table · pager · row drawer) ─────────────────────────
   The Files/Attachments dock settled what a dock's CONTENT should look like: a compact toolbar row, then a
   white bordered panel whose head carries the name + count + search, then a quiet-caps table with hairline
   row rules and a pager that always states the total. Every other dock that lists rows was free to invent
   its own table, and did — heavy borders, wide caps, no pager. These are the shared names so it can't drift
   again; the .fb-* rules inside RecordFiles.razor are the original of the recipe.

   Sizes match the .fb-* originals deliberately. If you change one, change both. */
.ci-dtoolbar { position: relative; display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: .9rem; }
.ci-dtoolbar-left { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; flex: 1 1 auto; min-width: 0; }
.ci-dtoolbar-right { display: flex; align-items: center; gap: .5rem; flex: 0 0 auto; margin-left: auto; }
/* A control and the word that names it, in ONE pill. This is what keeps a label from floating a row away
   from its own dropdown — the failure mode this block exists to end. */
.ci-dgroup { display: inline-flex; align-items: center; gap: .4rem; height: 34px; box-sizing: border-box;
             background: var(--ci-surface-2); border: 1px solid var(--ci-line); border-radius: var(--ci-radius); padding: 0 .45rem; color: var(--ci-ink-support);
             min-width: 0; transition: border-color var(--ci-transition), box-shadow var(--ci-transition); }
.ci-dgroup:focus-within { border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
.ci-dgroup-lbl { flex: none; color: var(--ci-ink-support); font-size: .7rem; font-weight: 700; text-transform: uppercase;
                 letter-spacing: .04em; padding-left: .15rem; white-space: nowrap; }
.ci-dgroup-n { flex: none; color: var(--ci-ink-faint); font-size: .72rem; white-space: nowrap; padding-right: .1rem; }

.ci-dpanel { min-width: 0; border: 1px solid var(--ci-line); border-radius: var(--ci-radius); background: var(--ci-surface); }
.ci-dpanel + .ci-dpanel { margin-top: .9rem; }
.ci-dpanel-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; padding: .6rem .8rem;
                  border-bottom: 1px solid var(--ci-line); }
.ci-dpanel-name { flex: none; min-width: 0; color: var(--ci-brand-ink); font-weight: 700; font-size: .88rem; }
.ci-dpanel-n { flex: 1 1 auto; color: var(--ci-ink-label); font-size: .74rem; }
/* Same 34px as .ci-dgroup: these two sit side by side in every toolbar, and a search box a few pixels shorter than
   the pill next to it made the whole row look accidental. Height here, not padding, so the box can't drift again. */
.ci-dsearch { flex: 0 1 13rem; min-width: 8rem; height: 34px; box-sizing: border-box; display: flex;
              align-items: center; gap: .35rem; background: var(--ci-surface);
              border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius); padding: 0 .5rem; color: var(--ci-ink-label); }
.ci-dsearch:focus-within { border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
.ci-dsearch input { flex: 1; min-width: 0; border: 0; outline: none; font: inherit; font-size: .82rem;
                    background: transparent; color: var(--ci-ink-body); }

/* The table scrolls sideways inside its own box — a record pane is narrow and the page itself must never pan. */
.ci-dtable-wrap { overflow-x: auto; }
.ci-dtable { width: 100%; border-collapse: collapse; font-size: .84rem; }
.ci-dtable th { text-align: left; padding: .45rem .7rem; color: var(--ci-ink-label); font-size: .64rem; font-weight: 700;
                text-transform: uppercase; letter-spacing: .06em; border-bottom: 1px solid var(--ci-line); white-space: nowrap; }
.ci-dtable td { padding: .5rem .7rem; border-bottom: 1px solid var(--ci-line-faint); vertical-align: top; color: var(--ci-ink-body); }
.ci-dtable > tbody > tr:last-child > td { border-bottom: 0; }
.ci-dtable > tbody > tr:hover > td { background: var(--ci-surface); }
/* Row actions live in ONE column, always the first, and it shrinks to its buttons (CLAUDE.md rule 12). */
.ci-dc-act { width: 1%; white-space: nowrap; }
.ci-dacts { display: flex; align-items: center; gap: .3rem; }
/* The row's subject, so it reads as the link it is. */
.ci-dname { display: block; max-width: 100%; background: transparent; border: 0; padding: 0; font-family: inherit;
            font-size: .86rem; font-weight: 700; color: var(--ci-accent-ink); text-align: left; cursor: pointer;
            overflow-wrap: anywhere; text-decoration: none; }
.ci-dname:hover { text-decoration: underline; }
.ci-dname-plain { color: var(--ci-ink-body); cursor: default; }
.ci-dname-plain:hover { text-decoration: none; }
.ci-dsub { color: var(--ci-ink-faint); font-size: .72rem; margin-top: .1rem; }
.ci-dwhen { color: var(--ci-ink-quiet); font-size: .78rem; white-space: nowrap; }
.ci-dempty { padding: 1.4rem .9rem; color: var(--ci-ink-quiet); font-size: .84rem; font-style: italic; text-align: center; }

/* ── Empty state (CiEmptyState) ──────────────────────────────────────────────────────────────────────────────────
   The shared answer to "this section has nothing in it": a quiet icon, a headline, one line of explanation, and the
   action that fills it — centred as one block. The action lives INSIDE, because a button sitting above an empty box
   reads as part of the panel's chrome and leaves the box looking like a dead end. Dashed like the dock's other
   placeholders (.ci-empty in dashboard-widgets.css) so an empty area is recognisable at a glance across the app. */
/* The quiet one-line note — "nothing here yet" in a panel too small to justify a full empty state, or a short aside
   under a control. It is NOT CiEmptyState: that is the icon + headline + action treatment for a section with nothing
   in it (.ci-es below). This is the single muted line for everywhere else.

   Consolidated 2026-08-23 from ELEVEN private copies of the same idea — .ins-empty, .sge-empty, .rd-empty,
   .nm-kid-empty, .sm-cand-empty, .id-empty, .tg-empty, .ln-empty, .md-note, .mc-note, .tw-note — which between them
   had drifted across FOUR different greys (#64748b, #8494ad, #90a4b8, #9ca3af) and FIVE font sizes (.78 to .9rem)
   for one visual concept, so no two panels' "nothing here" lines quite matched. Three of them were byte-identical to
   each other. If this is ever wrong, fix it here rather than starting a twelfth. */
.ci-note { color: var(--ci-ink-quiet); font-style: italic; font-size: .85rem; padding: .6rem 0; }
/* Tighter variant for a genuinely cramped spot — a tree child row, a narrow sidebar list. */
.ci-note-sm { font-size: .78rem; padding: .3rem .35rem; }

/* ── The empty state (CiEmptyState), in three sizes ────────────────────────────────────────────────────────────
   One shape, three homes: a widget/sidebar card (sm), a dock/tab/drawer (md — the default and what this looked
   like before sizes existed), a whole page or an empty list (lg). Everything except scale is constant, which is
   the point: the icon is always --ci-ink-disabled, the headline always brand ink, the help line always quiet, and
   both themes are decided once here. Absorbed the widgets' hand-written `.ci-empty` look-alike (2026-09-06). */
.ci-es { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .3rem;
         padding: 1.5rem 1rem; background: var(--ci-surface); border: 1px dashed var(--ci-line); border-radius: var(--ci-radius); text-align: center; }
.ci-es-ico { display: inline-flex; color: var(--ci-ink-disabled); margin-bottom: .1rem; }
.ci-es-title { color: var(--ci-brand-ink); font-size: .88rem; font-weight: 600; }
/* Capped so a sentence doesn't stretch the full width of a wide dock and stop reading as one line of help. */
.ci-es-msg { color: var(--ci-ink-quiet); font-size: .8rem; line-height: 1.4; max-width: 32rem; }
.ci-es-act { display: inline-flex; align-items: center; gap: .4rem; margin-top: .55rem; }
/* SM — inside a widget or a sidebar card, where the card's own chrome is right beside it. By convention it
   carries no action (the widget's head link is the way out); a caller may still pass one. */
.ci-es-sm { gap: .2rem; padding: .85rem .7rem; }
.ci-es-sm .ci-es-title { font-size: .8rem; }
.ci-es-sm .ci-es-msg { font-size: .74rem; }
.ci-es-sm .ci-es-act { margin-top: .4rem; }
/* MD — the default. Declared for symmetry so a size is always explicit in the markup. */
.ci-es-md { }
/* LG — a page, or a list with no rows. Room for the first thing somebody should do. */
.ci-es-lg { gap: .4rem; padding: 3rem 1.5rem; }
.ci-es-lg .ci-es-title { font-size: 1.05rem; }
.ci-es-lg .ci-es-msg { font-size: .88rem; max-width: 38rem; }
.ci-es-lg .ci-es-act { margin-top: .8rem; }
/* An empty state that IS the list: no double border inside the grid's own frame, centred like every other one.
   ★ GOTCHA: Syncfusion's empty row is one <td> in a table sized to the SUM OF THE COLUMNS, not to the visible
   area — so on any list wide enough to scroll sideways, a centred block centres against that scroll width and
   slides off to the right (measured: half of it clipped on the contracts list). The fix is upstream of the
   centring: when the grid has no rows there are no columns to honour, so THAT table alone is switched to
   `table-layout: auto` at 100% — the single full-span cell then measures the scroller, not the column sum, and
   ordinary centring lands true. Scoped with `:has(.e-emptyrow)` so a grid WITH rows is untouched.

   ★ DO NOT reach for container queries here. `container-type: inline-size` on `.e-content` also centres it, and
   it was shipped for about an hour — but containment applies to EVERY grid's scroller, not just an empty one, and
   the width it hands back feeds Syncfusion's own measuring: grids visibly flickered and bounced left-right
   (reported 2026-09-06). Containment on a third-party grid's scroll container is a layout feedback loop. */
/* min-height, not just padding: CiDataGrid renders this box EMPTY while the grid is binding and filled once
   DataBound has fired, so without a reserved height an empty list grew by ~80px the moment its message arrived —
   which on a view toggled back and forth (My Assignments' two scopes) reads as the grid expanding and contracting.
   One number for both states means only the words change. */
.ci-es-ingrid { border: 0; border-radius: 0; background: transparent; padding: 2.75rem 1.5rem; min-height: 9.5rem;
                box-sizing: border-box; }
.e-grid .e-gridcontent .e-table:has(.e-emptyrow) { table-layout: auto; width: 100%; }
.e-grid .e-gridcontent .e-table:has(.e-emptyrow) > colgroup { display: none; }
/* Superseded by Size="sm" (2026-09-06); kept so an un-migrated caller keeps its tighter padding. */
.ci-es-tight { padding: .9rem .7rem; }

.ci-dpager { display: flex; align-items: center; justify-content: flex-end; gap: .5rem; padding: .5rem .8rem;
             border-top: 1px solid var(--ci-line); }
.ci-dpager-n { flex: 1 1 auto; color: var(--ci-ink-faint); font-size: .74rem; }
.ci-dpager-size { display: inline-flex; align-items: center; gap: .3rem; color: var(--ci-ink-faint); font-size: .74rem; }
.ci-dpager-size select { border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius-sm); padding: .12rem .3rem; font: inherit;
                         font-size: .74rem; background: var(--ci-surface); color: var(--ci-ink-body); }
.ci-dpager-p { color: var(--ci-ink-2); font-size: .76rem; font-weight: 600; white-space: nowrap; }

/* ── Inline row drawer ──────────────────────────────────────────────────────────────────────────────────
   The compact panel that opens directly UNDER the row it belongs to — a file's older versions, a document's
   approvers or signers. It replaces the pattern of dropping a full-width panel BELOW the whole list, which
   put the detail nowhere near the row it described and pushed everything else off a laptop screen. The
   accent rail on the left is what ties the open drawer to the highlighted row above it. */
.ci-dtable > tbody > tr.is-open > td { background: var(--ci-surface-sunk); border-bottom-color: var(--ci-line-strong); }
.ci-rowdrawer > td { padding: 0 !important; background: var(--ci-surface-2); border-top: 1px solid var(--ci-line-strong); border-bottom: 1px solid var(--ci-line); }
/* Fades in with the same timing as the popovers. Opacity ONLY, no travel: this one opens IN the page (a real
   table row appears), so the layout below it moves regardless — adding movement to the content as well would be
   two things shifting at once. The fade just stops the detail arriving as a hard cut under the row you clicked. */
.ci-rowdrawer-in { display: block; border-left: 3px solid var(--ci-accent); padding: .65rem .8rem .75rem 1rem;
                   box-shadow: inset 0 2px 0 color-mix(in srgb, var(--ci-accent) 8%, transparent);
                   animation: ci-veil-in var(--ci-enter) ease-out; }
.ci-rowdrawer-head { display: flex; align-items: center; gap: .45rem; margin-bottom: .55rem; padding-bottom: .4rem;
                     border-bottom: 1px solid var(--ci-line); }
.ci-rowdrawer-t { color: var(--ci-ink-2); font-size: var(--ci-fs-panel); font-weight: var(--ci-fw-panel);
                  letter-spacing: var(--ci-ls-panel); }
.ci-rowdrawer-n { color: var(--ci-ink-meta); font-size: var(--ci-fs-meta); font-weight: var(--ci-fw-meta); }
.ci-rowdrawer-x { margin-left: auto; flex: none; display: inline-flex; align-items: center; justify-content: center;
                  width: 20px; height: 20px; border: 0; border-radius: var(--ci-radius-sm); background: transparent; color: var(--ci-ink-label);
                  cursor: pointer; font-size: .78rem; line-height: 1; padding: 0; }
.ci-rowdrawer-x:hover { background: var(--ci-hover-strong); color: var(--ci-brand-ink); }
/* A drawer holds a handful of rows; more than that scrolls inside it rather than stretching the record page. */
.ci-rowdrawer-scroll { max-height: 14rem; overflow-y: auto; }
/* The drawer's own mini table — tighter than the list it sits inside, and no header band (it would read as a
   second list rather than a detail of one row). */
.ci-rowdrawer .ci-dtable { font-size: .8rem; }
.ci-rowdrawer .ci-dtable th { padding: .2rem .55rem; background: transparent; border-bottom-color: var(--ci-line); }
.ci-rowdrawer .ci-dtable td { padding: .32rem .55rem; border-bottom-color: var(--ci-line-faint); vertical-align: middle; }
.ci-rowdrawer .ci-dtable > tbody > tr:hover > td { background: var(--ci-surface-sunk); }

/* ───────────────────────────── Modernization polish ─────────────────────────────
   Additive global refinements that lift every screen. These set properties (elevation, motion, focus
   rings) the per-component styles don't, so they layer on without overriding component colors/sizes.
   Brand stays Contract Insight blue. */

html { scroll-behavior: smooth; }
body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }

/* ── Buttons: flat, and the feedback is COLOUR, not movement ──────────────────────────────────────
   These buttons used to carry a resting drop shadow and rise 1px on hover. Both are gone, and the
   lift is the one worth explaining, because it looks like polish and isn't:
     · A page of this app puts fifteen to twenty buttons on screen (a header strip, a tab's action
       row, a drawer footer, a grid's row actions). Twenty shadowed rectangles is not depth, it is
       twenty smudges — and the shadow was under a 30px control, so it read as a soft edge rather
       than as elevation. Flat + a 1px border is what makes the row read as one crisp family.
     · The 1px rise moves the label under a cursor that is already on it, which is what makes a UI
       feel fidgety rather than quick, and it reflows nothing else — so the button appears to
       detach from the row it belongs to. A hover needs to answer "am I on it?", and a background
       shift answers that at a glance without anything moving.
   :active still gets a distinct state, because a press genuinely needs confirming — it just does
   it by going DARKER (see the per-variant hovers above) instead of by sliding back down.
   Anything genuinely floating over the page keeps its shadow; see --ci-shadow-lg. */
.ci-btn {border:none;}
.ci-btn, .ci-btn-secondary, .ci-btn-danger {
    transition: background var(--ci-transition), border-color var(--ci-transition), color var(--ci-transition), filter var(--ci-transition);
    box-shadow: none;
}
.ci-btn:active, .ci-btn-secondary:active, .ci-btn-danger:active { filter: brightness(.94); }
.ci-btn:disabled, .ci-btn-danger:disabled { filter: none; }

/* Cards / surfaces: flat, separated by their own hairline. The shadow that used to sit under every
   card here is deliberately NOT replaced by a fainter one — a card sits IN the page, so it gets an
   edge, not a lift (the reasoning is with --ci-shadow-sm). The radius still comes from the token. */
.record-card, .file-table, .note-card { box-shadow: none; }
.record-card { border-radius: var(--ci-radius); }

/* Form inputs: consistent focus ring across text/select/textarea (matches the search box). */
.field-input:focus, .note-subject:focus, .note-body:focus, .note-visibility:focus, .file-keywords:focus {
    outline: none; border-color: var(--ci-accent); box-shadow: var(--ci-ring);
}

/* A native <select> with no author background inherits the platform's button face. Chrome's happens to be white,
   Firefox's is grey — so on Firefox every dropdown read as locked next to our white text inputs, which is a
   meaningful thing to get wrong: "greyed out" is how this app signals a field you may not edit.
   State the surface explicitly so enabled looks the same everywhere, and give :disabled a distinctly different
   fill so the real thing still reads as unavailable.
   Syncfusion controls (.e-control) render their own markup and own their theming — left alone. */
select:not(.e-control) {
    background-color: var(--ci-surface);
    color: inherit;
}
select:not(.e-control):disabled {
    background-color: var(--ci-surface-sunk);
    color: var(--ci-ink-faint);
    cursor: not-allowed;
}

/* Tabs: smoother transition on the underline/hover. */
.ci-tab { transition: background var(--ci-transition), color var(--ci-transition), border-color var(--ci-transition); }

/* A field group the client configured to sit collapsed for a record like this one (legacy's collapsible field
   groups). Quieter, never hidden: the record still HAS that section and somebody may want it, so this dims the tab
   and leaves it a click away. Full strength again the moment it is the open tab — once you are looking at it, telling
   you it is unimportant is just in the way. */
.ci-tab.is-quiet { opacity: .55; }
.ci-tab.is-quiet:hover, .ci-tab.is-quiet.active { opacity: 1; }

/* Pre-mount placeholder for a host that will mount a heavy third-party component (the Stimulsoft viewer/designer).
   Shown only BEFORE that component exists — during prerender, or while its definition loads. Once mounted, the
   component shows its OWN centred spinner (`.sti-component-loader`, z-index 1000001), so do NOT also overlay this one
   or you get a spinner inside a spinner.
   The geometry deliberately matches theirs (64px / 6px ring) so the handover doesn't visibly resize.
   IMPORTANT: their loader is `position: absolute; width/height: 100%` inserted inside the component element, so it
   centres against the nearest POSITIONED ancestor — the host must be `position: relative` or their spinner renders
   off-centre against something further out. That, not this placeholder, is the actual centring fix. */
.ci-load-veil { position: absolute; inset: 0; display: grid; place-items: center; background: var(--ci-surface); }
.ci-load-spin { width: 64px; height: 64px; border-radius: 50%; box-sizing: border-box;
                border: 6px solid rgba(44, 64, 112, .2); border-left-color: var(--ci-line-emphasis);
                animation: ci-spin 1.1s linear infinite; }
@keyframes ci-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ci-load-spin { animation-duration: 3s; } }

/* An in-BUTTON spinner, for an action that takes long enough to need saying so but keeps its own button — "Opening…"
   on the record drawers' Edit, where turning fifty read-only fields into editors means fetching every picker's option
   list. Sits where the button's icon was, inherits the button's text colour, so it works on filled and outlined alike.
   Use this rather than a private copy per page: it existed as three slightly different circles before. */
.ci-btn-spin { display: inline-block; width: 13px; height: 13px; flex: none; box-sizing: border-box; border-radius: 50%;
               border: 2px solid currentColor; border-right-color: transparent; animation: ci-spin .7s linear infinite; }
@media (prefers-reduced-motion: reduce) { .ci-btn-spin { animation-duration: 2s; } }

/* Sub-tabs: an in-page engine/section switch under a CiPageHeader (e.g. Reports and Dashboards splitting their
   standard list from the graphical one). Underlined, not pill-shaped, so they read as secondary to the page title. */
/* The views of ONE admin/setup screen — a folder tab per view, the active one joined to the content below it.
   Used by the setup and dashboard screens (document templates, compliance, reports, system dashboards, …); a RECORD's
   tabs are .ci-tabs/.ci-tab and are deliberately a different, flatter control, because a record's tab strip carries
   twenty of them and scrolls.
   These were flat blue text with an underline, which read as links rather than as a set of views you switch between. */
/* flex-shrink: 0 because the strip is furniture on a .ci-list-wrap page (U9) — without it the grid below
   claims the strip's height as it fills, and the tabs squash. */
.ci-subtabs { display: flex; align-items: flex-end; gap: .25rem; flex-wrap: wrap; flex-shrink: 0;
              border-bottom: 1px solid var(--ci-line-strong); margin-bottom: .8rem; }
.ci-subtab { display: inline-flex; align-items: center; gap: .35rem; margin-bottom: -1px;
             background: var(--ci-surface-sunk); border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius) var(--ci-radius) 0 0;
             padding: .32rem .7rem; cursor: pointer;
             color: var(--ci-ink-support); font: inherit; font-size: .84rem; font-weight: 600;
             transition: color var(--ci-transition), background var(--ci-transition), border-color var(--ci-transition); }
.ci-subtab:hover { color: var(--ci-brand-ink); background: var(--ci-hover); }
/* The active tab is the page under it: white, its bottom border replaced by the panel, and an accent edge on top. */
.ci-subtab.is-on { background: var(--ci-surface); color: var(--ci-brand-ink); border-bottom-color: var(--ci-surface);
                   box-shadow: inset 0 2px 0 var(--ci-accent); }
.ci-subtab:focus-visible { outline: none; border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
/* A view can be unavailable while a job runs — it stays visible and disabled (rule 10), never hidden. */
.ci-subtab:disabled { opacity: .6; cursor: default; }
.ci-subtab:disabled:hover { background: var(--ci-surface-sunk); color: var(--ci-ink-support); }
.ci-subtab.is-on:disabled:hover { background: var(--ci-surface); color: var(--ci-brand-ink); }
.ci-subtab-n { background: var(--ci-hover); color: var(--ci-ink-support); border-radius: var(--ci-radius-pill); font-size: .7rem; padding: .02rem .34rem; }

/* ── THE NUMBERED STEP RAIL (.ci-steps) ─────────────────────────────────────────────────────────────────────────
   Where you are in an ordered sequence: a numbered disc per step with the connecting line drawn between them, the
   current one filled accent, the ones behind it outlined accent. Use it for a WIZARD (a send, an import) and for a
   setup broken into stages; use `.ci-subtabs` when the sections have no order and you may visit them in any order.

   Shared as of 2026-09-06. It lived privately in DocTemplateWorkbench as `.tw-step*`, and the e-signature send
   wizard had drifted to a plain "1. Documents  2. Recipients" text line — two takes on the same idea, which is
   exactly what U2 exists to stop. The rules below are that page's, moved verbatim so nothing about the workbench
   changed; ESignSend now renders the same markup.

   Compact on purpose: it is a signpost, not content, and it must not cost the height of a real toolbar on a
   722px laptop. `flex: none` because these pages are `.ci-list-wrap` flex columns — furniture between the header
   and the content must not be squeezed when a grid claims height (U9).
   A step may be a <button> (clickable, as the workbench's stages are) or a plain <span> (a wizard you must walk in
   order, as the send is) — both style identically, so the markup says which without a second class. */
.ci-steps { display: flex; align-items: flex-start; gap: 0; margin: 0 0 .7rem; flex-wrap: wrap; flex: none; }
.ci-step { position: relative; flex: 1 1 0; min-width: 7rem; display: flex; flex-direction: column;
           align-items: center; gap: .15rem; background: none; border: 0; padding: .1rem .3rem .2rem;
           color: var(--ci-ink-quiet); font: inherit; font-size: .74rem; font-weight: 600; text-align: center; }
button.ci-step { cursor: pointer; }
/* The line between steps: drawn from each step except the first, back to the one before it, so it can never end
   in mid-air after a wrap. */
.ci-step:not(:first-child)::before { content: ""; position: absolute; top: .68rem; right: 50%; left: -50%;
                                     height: 2px; background: var(--ci-hover-strong); }
.ci-step-n { position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center;
             width: 1.4rem; height: 1.4rem; border-radius: var(--ci-radius-pill); background: var(--ci-surface);
             border: 2px solid var(--ci-line); color: var(--ci-ink-quiet); font-size: .72rem; font-weight: 700; }
.ci-step-l { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The disc on its own, FILLED — for a page whose steps are stacked sections rather than a rail, where the number
   rides in each section's heading (Mass Process Job Setup). Same disc, so the product has one numbered step marker
   and not one per screen; filled because at heading size an outlined grey disc reads as disabled next to bold ink. */
.ci-step-n.is-filled { background: var(--ci-navy); border-color: var(--ci-navy); color: var(--ci-ink-on-brand); }
button.ci-step:hover { color: var(--ci-brand-ink); }
button.ci-step:hover .ci-step-n { border-color: var(--ci-line-strong); }
.ci-step.is-done .ci-step-n { border-color: var(--ci-accent); color: var(--ci-accent-ink); }
.ci-step.is-on { color: var(--ci-brand-ink); }
.ci-step.is-on .ci-step-n { background: var(--ci-accent); border-color: var(--ci-accent); color: var(--ci-ink-on-brand); }
.ci-step:focus-visible { outline: none; }
.ci-step:focus-visible .ci-step-n { box-shadow: var(--ci-ring); }

/* ★ A "MANY STEPS" scrolling variant and per-step outcome colours (is-ok / is-warn / is-skip) were added here on
   2026-09-06 for the Launchpad and REMOVED the same day when it went back to its own strip. Recorded so nobody
   rebuilds them for the same reason: the Launchpad's twenty setup areas are a CHECKLIST worked in any order, and
   this rail's connecting line asserts a sequence. If a genuine flow ever needs twenty ordered steps, add them
   back — but first check the flow is ordered, because that is the thing that was wrong the first time. */

/* Below the tablet break the labels can't fit side by side; the numbers still tell you where you are. */
@media (max-width: 900px) { .ci-step-l { display: none; } .ci-step { min-width: 2.6rem; } }

/* THE required-field mark. One definition, because it had already forked five ways — `.req` on the record form plus
   private `nth-req` / `pe-req` / `it-req` / `ri-req` on four bespoke screens, each its own colour and spacing — while
   145 of the app's 154 bespoke pages marked nothing at all, including screens that refuse the save when the field is
   empty. A field the form will reject has to say so BEFORE the save, not after it.

   Deliberately small: a red asterisk after the label, no bold, no larger size. It is a hint, not a warning — the
   warning is what the save says when the field is still empty.

   `--ci-danger` and not a literal, so it holds up in dark mode; `.1rem` of space so it hugs the label rather than
   floating between words. Marked aria-hidden on purpose: the ACCESSIBLE signal is the control's own `required` /
   `aria-required` attribute, and a screen reader announcing "asterisk" after every label is noise on top of it. */
.ci-req { color: var(--ci-danger); margin-left: .1rem; font-weight: inherit; }

/* Where a whole form is optional-by-default and only one or two fields are mandatory, the same mark still applies —
   there is no "(optional)" convention in this app, and two ways of saying it would be worse than either. */
.ci-subtab.is-on .ci-subtab-n { background: var(--ci-surface-sunk); color: var(--ci-accent-ink); }

/* A graded value pill — a rating or a status where the COLOUR is the message: red worst, green best, grey unknown.
   Used by the compliance screens (OFAC/SAM confidence, D&B financial strength and risk indicator, review status) and
   by the record's screening dock. Shared rather than page-local because the same grade has to look the same
   everywhere it is shown; the band name comes from ComplianceReviewService.BandFor*, so it can't drift per screen. */
.ci-band { display: inline-flex; align-items: center; height: 1.3rem; padding: 0 .45rem; border-radius: var(--ci-radius-sm);
           font-size: .7rem; font-weight: 700; color: var(--ci-ink-on-brand); white-space: nowrap; }
.ci-band-red { background: #c0392b; }
.ci-band-yellow { background: #d9a406; }
.ci-band-green { background: #2e9e5b; }
.ci-band-gray { background: var(--ci-line-strong); }

/* Accessibility: a visible focus ring for keyboard users on links/buttons that don't already define one. */
a:focus-visible, button:focus-visible, .ci-nav-link:focus-visible {
    outline: 2px solid var(--ci-accent); outline-offset: 2px; border-radius: var(--ci-radius-sm);
}

/* Refined, unobtrusive scrollbars in the content + sidebar (WebKit/Chromium + Firefox). */
.ci-content, .ci-nav, .e-grid .e-content { scrollbar-width: thin; scrollbar-color: #c2ccda transparent; }
.ci-content::-webkit-scrollbar, .ci-nav::-webkit-scrollbar { width: 10px; height: 10px; }
.ci-content::-webkit-scrollbar-thumb { background: var(--ci-line-strong); border-radius: var(--ci-radius); border: 2px solid var(--ci-content-bg); }
.ci-content::-webkit-scrollbar-thumb:hover { background: var(--ci-line-strong); }
.ci-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); border-radius: var(--ci-radius); }

/* ── "There is more table to the right" ─────────────────────────────────────────────────────────────────────
   A wide table clipped at its container's right edge looks exactly like a table that ends there, so people
   read a clipped list as the whole list. This paints a soft fade against the right edge that DISAPPEARS on
   its own once you reach the end.

   No script, and it cannot disagree with the real scroll position, which is the whole reason for the shape:
     · the SHADOW layer is `scroll`-attached — pinned to the element's own right edge, so it stays put.
     · the WHITE layer is `local`-attached — it travels with the content, sitting against the CONTENT's right
       edge. Scroll to the end and it slides over the shadow and hides it. Better still, a table with nothing
       to scroll has its right edge already at the box's right edge, so the white layer covers the shadow from
       the start and a non-scrolling grid shows nothing. The affordance appears exactly when it is true.

   Two details that are easy to get wrong:
     · `repeat-y` on the white layer, not `no-repeat`. A grid that scrolls VERTICALLY as well (any FillHeight
       list) has a local layer that scrolls up out of view with the rows, and the shadow would reappear at the
       end of a long table. Tiling it down the axis covers every scroll position.
     · `background-image` and friends are set as LONGHANDS, never the `background` shorthand, because on
       .e-content that would also blow away Syncfusion's own background-color and the rows would go
       transparent.

   Strength — three settings were retuned in sequence, and the SHAPE turned out to matter more than either number:
     1. rgba(44,64,112,.3) over .85rem, radial — the original. A hard bar ruled down the edge of the table.
     2. .18 over 1.35rem, radial — paler and wider. Still too heavy, and the extra width made it worse: a fade
        reaching 1.35rem in starts covering the last column's text rather than just its edge.
     3. .11 over .8rem, LINEAR — where it landed. The radial gradient was the real problem: `farthest-side at
        100% 50%` concentrates its ink into a dark core at the vertical middle of the edge and falls off in an
        arc, so it reads as a drop shadow cast by something — an object — rather than as content running out. A
        linear ramp spreads the same (now much smaller) amount of ink evenly down the whole edge, so it reads as
        a fade. That is why this is faint AND narrow now, where step 2 tried to buy softness with width.
   It only has to say "keep going" — the always-visible scrollbar is the precise signal. If it ever needs
   retuning again, these three lines are the whole control surface; don't touch the gradients below. */
:root {
    --ci-xfade-c: rgba(44, 64, 112, .07);   /* the fade's darkest point, at the very edge */
    --ci-xfade-w: .8rem;                    /* how far it reaches in — keep it off the last column's text */
    --ci-xfade-bg: #fff;                    /* the surface it fades FROM — the scroller's own background */
    /* There WAS a second, stronger pair here for the Syncfusion grid tier (--ci-xfade-c-grid / -w-grid). The grid
       fade they painted is gone as of 2026-08-28 — replaced by the .ci-gridx-rail edge below — so the tokens went
       with it rather than sit here unreferenced. Worth keeping the finding that forced them apart, in case a grid
       overlay is ever attempted again: the .ci-xscroll fade sits UNDER the content on the scroller's own white, so
       a whisper of ink reads, while an overlay ON TOP of opaque striped rows needs roughly three times the ink for
       the same result — at .11/.8rem it computed correctly (opacity 1, right z-index, animation live) and was
       still invisible on screen, which is why the grid fade looked like it was "not working" at all. */
}
.ci-xscroll {
    background-image:
        linear-gradient(to left, var(--ci-xfade-bg), rgba(255, 255, 255, 0)),
        linear-gradient(to right, rgba(44, 64, 112, 0), var(--ci-xfade-c));
    background-position: right center, right center;
    background-size: 2rem 100%, var(--ci-xfade-w) 100%;
    background-repeat: repeat-y, repeat-y;
    background-attachment: local, scroll;
}
/* ── The same affordance on a Syncfusion GRID, which needs a different mechanism ──────────────────────────────
   The background trick above CANNOT work here, and it is worth writing down because it looks like it should:
   .e-grid's <table>, every <tr> and every <td> carry their own OPAQUE background (measured — the table is
   rgb(255,255,255), rows rgb(247,249,252), cells rgb(244,247,251); that is how the zebra striping and the
   frozen columns work). A background on the scroll container is painted behind all of that and is never seen.

   So on a grid the fade is an OVERLAY — a pseudo-element over the rows — and its fade-out is driven by a
   scroll-linked animation instead of by a second background layer:
     · .e-content names a scroll timeline on its INLINE axis;
     · .e-grid puts that name in scope (timeline-scope) so a sibling subtree can reference it;
     · .e-gridcontent::after animates opacity 1 → 0 across that scroll range.

   The base state is `opacity: 0` and the animation supplies the visible end, which is what makes this behave
   correctly in the four cases that matter — all of them fall out of the CSS with no measuring and no script:
     · a grid with nothing to scroll — the timeline is INACTIVE, so the animation does not apply and the base
       wins: no fade. This is the case a naive overlay gets wrong, painting a "there's more" hint on a grid that
       has no more.
     · DURING LOAD, before the grid has built its scroller, that timeline does not exist yet — and the fill mode
       is what decides whether the base still wins. It must be `forwards`, NOT `both`: with `both` the animation
       is in its BEFORE phase and fills the FIRST keyframe, which is the visible one, so every grid flashed the
       fade while it rendered and then lost it a moment later when the timeline resolved. On a grid that turns
       out to fit, that flash was the only fade you ever saw. `forwards` fills only after the active phase, so
       the before phase falls through to `opacity: 0` and the hint appears once, when it is true.
     · scrolled to the start — progress 0 — full fade.
     · scrolled to the end — progress 1 — no fade.
   A browser without scroll-driven animations (Safari, Firefox today) also lands on the base and shows no fade,
   which is the safe direction to degrade: the always-visible scrollbar still says there is more, and nobody
   gets a hint that cannot go away.

   It sat on .e-gridcontent, not .e-grid, so it spanned exactly the rows — not the navy header band above them.
   pointer-events: none, or it would have eaten clicks on the last column.

   SUPERSEDED 2026-08-28 by the edge rail below, and the scroll-timeline fade is GONE — see the history note there
   before reaching for a gradient again. The mechanism is left described because .ci-xscroll above still uses the
   background form of it, and because the `forwards`-not-`both` trap is the kind of thing that gets re-broken. */

/* ── A wide grid's right EDGE: .ci-gridx-rail ────────────────────────────────────────────────────────────────
   THE HISTORY, because three cues were built here before this one and the reasons they failed are the design:
     · a gradient FADE on the scroll edge (the block described above) — shipped, and not enough on its own. On
       /reports it was 17.5px of gradient against four columns sitting past the edge; nobody read it as "there is
       more this way", they read it as the table being slightly blurry.
     · a "More columns →" LABEL at the top-right of the rows — rejected on sight. It read as a box dropped on top
       of the first row. The header band is no better: the columns fill that width, so it covers a column's NAME.
     · a small CHEVRON at the bottom-right — rejected on sight. It lands directly above the VISDOM chat launcher,
       and two round marks stacked in one corner look like one broken control.
   The common failure is that all three FLOAT OVER the rows, so they belong to nothing and read as damage. This
   one isn't an overlay at all: it takes 15px as a GUTTER (the host gains padding, the grid re-lays-out narrower),
   so it never covers a value, and it becomes the card's own right edge — the grid gives up its right border and
   right rounding, and the rail supplies both. That is why it reads as structure rather than as a mark.
   It is also a CONTROL, not a hint: pressing it scrolls a screenful across, and at the far right it turns around
   and goes back rather than vanishing (vanishing would drop the gutter and shift the grid 15px sideways at the
   exact moment you finished scrolling to it). Its tooltip counts the columns out loud — "Show the 4 columns off
   to the right" — which is the sentence that answers the missing-column bug report.
   .has-xcols / .xcols-end come from ci-grid-edge.js, which measures the overflow; there is no CSS test for it. */
/* border-box because the gutter must come OUT of the host's width, never be added to it — there is no global
   box-sizing reset in this stylesheet, so a call site that gives the host an explicit width would otherwise push
   it 15px past its container. */
.ci-grid-host { --ci-gridx-rail-w: 15px; position: relative; box-sizing: border-box; }
.ci-gridx-rail { display: none; }
.ci-grid-host.has-xcols { padding-right: var(--ci-gridx-rail-w); }
/* The grid stops being the card's right edge and hands that job to the rail, so the two abut with no seam. */
.ci-grid-host.has-xcols > .e-grid { border-right: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.ci-grid-host.has-xcols > .ci-gridx-rail {
    display: flex; position: absolute; top: 0; right: 0; bottom: 0; width: var(--ci-gridx-rail-w);
    align-items: center; justify-content: center; padding: 0;
    border: 1px solid var(--ci-line); border-left-color: var(--ci-line-strong);
    border-radius: 0 var(--ci-radius-lg) var(--ci-radius-lg) 0;
    /* A tint of the header's own navy rather than --ci-surface-sunk. Measured in both themes: sunk (#f5f8fb) against
       the rows' white and #f8fafc stripe is a ~1% difference — the rail was there and invisible, which is precisely
       how the fade, the label and the corner chevron all failed. The formula reads in dark mode too, where sunk
       happens to be DARKER than the surface, so one declaration covers both (U12). */
    background: color-mix(in srgb, var(--ci-navy) 10%, var(--ci-surface)); color: var(--ci-brand-ink);
    cursor: pointer; transition: background var(--ci-transition), color var(--ci-transition);
}
.ci-grid-host.has-xcols > .ci-gridx-rail:hover { background: color-mix(in srgb, var(--ci-navy) 18%, var(--ci-surface)); }
.ci-grid-host.has-xcols > .ci-gridx-rail:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: -2px; }
/* At 10px the shared CiIcon stroke (1.9 against a 24 viewBox) renders as a grey smudge — this is the one place
   the icon set is overridden, and only its weight. */
.ci-gridx-rail .ci-icon { stroke-width: 3.2; }
/* Scrolled to the end: the same rail, turned around. */
.ci-grid-host.xcols-end > .ci-gridx-rail .ci-icon { transform: rotate(180deg); }

/* The scrollbar STAYS VISIBLE on a horizontal scroller. It is the one universal "there is more this way"
   signal, and an overlay scrollbar hides until you already scrolled — which is the problem, not the fix. */
.ci-xscroll { scrollbar-width: thin; scrollbar-color: #c2ccda transparent; }
.ci-xscroll::-webkit-scrollbar { height: 10px; }
.ci-xscroll::-webkit-scrollbar-track { background: var(--ci-surface-sunk); border-radius: var(--ci-radius); }
.ci-xscroll::-webkit-scrollbar-thumb { background: var(--ci-line-strong); border-radius: var(--ci-radius); }
.ci-xscroll::-webkit-scrollbar-thumb:hover { background: var(--ci-line-strong); }

/* ── Click-to-place ghost (the two signature-layout canvases; ci-field-drag.js positions it) ─────────────────
   The armed field's TRUE outline follows the pointer over a page that accepts click-to-place, so its size and
   centre are visible BEFORE the click. QA (2026-09-04): people aim with the middle of an invisible box whose
   top-left was the anchor, and every field type is a different size — drops landed low-right of the intent.
   Tokens only, so both themes get it for free. pointer-events:none keeps every click on the page itself. */
.ci-place-ghost { position: absolute; left: 0; top: 0; display: none; align-items: center; justify-content: center;
    pointer-events: none; border: 1.5px dashed var(--ci-accent);
    background: color-mix(in srgb, var(--ci-accent) 12%, transparent); color: var(--ci-accent-ink);
    font-size: .68rem; font-weight: 600; letter-spacing: .02em; border-radius: var(--ci-radius-xs);
    z-index: 1; overflow: hidden; white-space: nowrap; }

/* ── Entrances: everything that ARRIVES over the page ────────────────────────────────────────────────────────
   Before this, one surface in the whole app animated in (the toast) and one faded (the drawer). Every menu,
   popover, dropdown and dialog appeared instantly — which is the single biggest thing that made the UI read as
   unfinished next to a polished SaaS product, and it is invisible in a screenshot, which is why it survived
   several passes.

   Why an entrance is not decoration: a panel that appears with no motion gives the eye no clue where it came
   from, so you re-scan the screen to work out what just changed. 4px of travel from the direction the panel is
   anchored answers that before you consciously look — the menu grew out of the button you pressed. That is the
   whole job, and it is why the travel is TINY and the duration is short: the motion is a pointer, not a show.

   FOUR shapes only, matched to what the surface is:
     ci-pop-in    a panel anchored to a control (menu, popover, dropdown). Drops 4px from above, because these
                  hang below their button — it reads as unfolding downward from it.
     ci-dlg-in    a centred dialog. Nothing to grow out of, so it scales up from .98 instead — a rise or a drop
                  would imply an off-screen origin it does not have.
     ci-veil-in   a scrim. Opacity only. It has no shape and no origin; moving it would be nonsense.
     ci-rise-in   page content ASSEMBLING — a dashboard's widget cells arriving as the page builds (owner call,
                  2026-09-05). A 6px rise, because the content settles INTO the page rather than hanging off a
                  control. The owner staggers its members with animation-delay and uses fill-mode `backwards` so
                  a delayed cell is invisible until its turn; under reduced motion swap it for ci-veil-in at the
                  site (the shared reduced-motion list below only covers the floating panels).

   Still four. `.ci-swap-a` / `.ci-swap-b` further down are NOT a fifth shape — they are ci-veil-in's fade with a
   restart mechanism, for content REPLACED IN PLACE (a tab panel) rather than added to the page. See the comment
   there before reaching for a new keyframe.

   Applied with `animation`, NOT `transition`, deliberately: these elements are added to the DOM when they open
   (Blazor renders them on a flag), and a transition needs a start state to move FROM on an element that already
   exists. An animation runs on first paint, which is exactly the moment we care about.

   No EXIT animation, and that is a decision rather than an omission: Blazor removes the element from the render
   tree the instant the flag flips, so an exit would need the component to keep the node alive and manage a
   closing state — real complexity in a dozen components, to animate a thing the user has already dismissed and
   stopped looking at. Opening is where the eye needs help; closing is not. */
@keyframes ci-pop-in  { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
/* NOTE the translate(-50%, -50%) in BOTH frames. `.ci-dlg` is centred with that transform, and `transform` is a
   single property — a keyframe animating a bare `scale(.98)` REPLACES the centring and throws the dialog to the
   bottom-right corner mid-animation. It has to carry the centring through every frame. */
@keyframes ci-dlg-in  { from { opacity: 0; transform: translate(-50%, -50%) scale(.98); }
                        to   { opacity: 1; transform: translate(-50%, -50%); } }
@keyframes ci-veil-in { from { opacity: 0; }                             to { opacity: 1; } }
@keyframes ci-rise-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* Anchored panels — the header menus (apps / + Add / account), the five utility-glyph popovers, a grid row's
   kebab, a list-view menu, the collapsed rail's flyout, the presence and type-picker pops. */
.ci-hmenu-panel,
.ci-bell-panel, .ci-ann-panel, .ci-tut-panel, .ci-help-panel, .ci-conf-panel,
.ci-rowmenu-panel, .ci-lv-menu, .ci-mb-menu, .ci-nav-flyout,
.ci-presence-pop, .ci-tp-pop, .ci-ph-pop,
/* The field-settings popovers on the two signature-placement canvases, anchored at the selected field. */
.wph-props, .sph-props,
/* .ci-editbar is the one member of this list that is IN the flow rather than floating over it, and it belongs here
   anyway: it unfolds downward from the control that opened it, which is exactly what this shape means. */
.ci-editbar {
    animation: ci-pop-in var(--ci-enter) var(--ci-enter-ease);
}
/* A dialog and its scrim. The scrim fades on its own so the page dims INTO the dialog rather than the two
   arriving as one hard cut.
   The DRAWER is deliberately not in the pop-in list and keeps its own fade: `.ci-drawer` must never carry a
   transform, because that makes it the containing block for its fixed-positioned descendants and Syncfusion
   then places body-appended popups (a date picker's calendar) on the far side of the screen. The existing
   comment in CiDrawer.razor records that trade — a slide-in is not worth it. Its scrim can still fade. */
.ci-dlg { animation: ci-dlg-in var(--ci-enter) var(--ci-enter-ease); }
.ci-dlg-scrim, .ci-drawer-scrim { animation: ci-veil-in var(--ci-enter) ease-out; }

/* Content REPLACED IN PLACE — a record's tab panel. This is NOT a fifth shape: nothing arrives over the page and
   nothing leaves it, the same box simply shows something else, so it is `ci-veil-in`'s opacity-only fade. What an
   in-place swap needs on top is a way to RE-RUN. The panel is re-rendered on a tab click, not recreated, and an
   animation fires when an element is painted for the first time — a single class would animate the first tab you
   land on and nothing after it. Two classes running identical keyframes give it that: alternating them changes the
   animation-NAME, which is the one thing that restarts an animation on an element that is already there. The owner
   flips a bool on every switch (RecordEditView._tabSwap).
   Why bother at all: twenty tabs across one strip, all the same size, all the same background. With no cue the
   swap reads as the screen having glitched rather than as you having changed view, which is the single most common
   note on a tabbed record page.
   Reduced motion needs no variant here — the fade IS the whole animation, there is no travel to drop, and the
   shared rule below is deliberately about keeping fades. */
@keyframes ci-swap-a { from { opacity: 0; } to { opacity: 1; } }
@keyframes ci-swap-b { from { opacity: 0; } to { opacity: 1; } }
.ci-swap-a { animation: ci-swap-a var(--ci-swap) var(--ci-enter-ease); }
.ci-swap-b { animation: ci-swap-b var(--ci-swap) var(--ci-enter-ease); }

/* A grid row that JUST changed — added, or saved — holds a tint and lets it go. The record form has answered
   "what did I just change?" this way for a while (.field-flash / .field-saved on RecordEditView); a grid answered
   it with nothing, so a row added to a sub-table appeared somewhere among fifty sorted rows with no way to tell
   which one was yours.
   It goes on the CELLS, not the row. Measured on a live grid: the row itself is transparent and so are most of its
   cells, but the row-actions cell is not — `.e-grid .e-rowcell.ci-actcol` paints `--ci-surface` so the action
   buttons keep a solid ground while the grid scrolls sideways. A tint on the <tr> would therefore colour the whole
   row EXCEPT the first column, which looks like a rendering fault rather than a highlight.
   It fades out to `transparent`, never to a surface colour, so the last frame hands the row back whatever
   background it actually has — striped, hovered, selected, or a deadline cue. Ending on `--ci-surface` would
   flatten all four to plain white the moment the animation finished.
   It HOLDS before it fades (the 35% stop), which is what separates a flash from a fade. Every other entrance here
   uses --ci-enter-ease, a fast-out curve that is right for something arriving and wrong for this: on that curve the
   tint has mostly gone within a fifth of a second, so by the time the eye gets back from the drawer that just
   closed there is nothing left to find. Holding first, then releasing slowly, is the whole point.
   No reduced-motion variant, same reasoning as the swap: a colour fade has no travel, and dropping it would take
   away the answer rather than the motion. */
@keyframes ci-row-flash {
    0%, 35% { background-color: color-mix(in srgb, var(--ci-accent) 26%, transparent); }
    100%    { background-color: transparent; }
}
.e-grid .e-row.ci-row-flash > .e-rowcell { animation: ci-row-flash var(--ci-flash) ease-out; }

/* Reduced motion: keep the FADE, drop the travel. The fade is what says "this is new"; the movement is the part
   that causes trouble for people who asked not to have it. Going fully instant would take away information. */
@media (prefers-reduced-motion: reduce) {
    .ci-hmenu-panel,
    .ci-bell-panel, .ci-ann-panel, .ci-tut-panel, .ci-help-panel, .ci-conf-panel,
    .ci-rowmenu-panel, .ci-lv-menu, .ci-mb-menu, .ci-nav-flyout,
    .ci-presence-pop, .ci-tp-pop, .ci-editbar,
    .ci-dlg { animation: ci-veil-in var(--ci-enter) ease-out; }
}

/* Print: drop the app chrome + the record's contextual sidebar/actions so a record prints cleanly. */
@media print {
    .ci-sidebar, .ci-topbar, .record-side, .head-actions, .ci-tabs { display: none !important; }
    .ci-shell, .ci-content, .ci-main { display: block !important; height: auto !important; overflow: visible !important; }
    .record-layout { display: block !important; }
    .record-card { border: 0 !important; box-shadow: none !important; padding: 0 !important; }
}

/* ── Syncfusion: adopt the app typeface ─────────────────────────────────────────────────────────
   The bootstrap5 theme hardcodes its own system-ui stack in 60+ rules — not just on the control roots
   but on inner text elements too — so setting a font on <body> alone left every grid, picker, dialog
   and chip rendering in a different face than the page around them. These are the same selectors the
   theme uses, re-pointed at ours; cobblestone-theme.css loads after bootstrap5.css, so equal
   specificity is enough and nothing here needs !important.
   Deliberately NOT a blanket `.e-control *` sweep: the theme sets font-family:"e-icons" on ~200
   selectors to render its glyphs, and a wide rule that outspecified any of them turns an icon into a
   letter. Every selector below is a text-bearing element, so the icon rules are never in play. */
.e-control, .e-css, .e-error,
input.e-input, textarea.e-input, .e-input-group, .e-input-group.e-control-wrapper,
.e-float-input, .e-float-input.e-control-wrapper,
.e-btn, .e-css.e-btn,
.e-grid, .e-card, .e-listview, .e-badge, .e-avatar, .e-message,
.e-dropdownbase .e-list-item, .e-dropdownbase .e-list-group-item, .e-fixed-head,
.e-popup.e-ddl .e-dropdownbase.e-nodata,
.e-checkbox-wrapper .e-label, .e-css.e-checkbox-wrapper .e-label,
.e-radio + label .e-label,
.e-switch-wrapper .e-switch-on, .e-switch-wrapper .e-switch-off,
.e-tooltip-wrap .e-tip-content,
.e-contextmenu-wrapper ul.e-ul, .e-contextmenu-container ul.e-ul,
.e-tab .e-tab-header .e-toolbar-item .e-tab-text,
.e-multi-select-wrapper input[type=text], .e-multi-select-wrapper .e-delim-values,
.e-multi-select-wrapper .e-chips > .e-chipcontent,
.e-listbox-wrapper, .e-listbox-container, .e-listboxtool-wrapper,
.e-splitter.e-splitter-horizontal .e-pane, .e-splitter.e-splitter-vertical .e-pane,
.e-control-wrapper.e-mask .e-maskedtextbox,
.e-upload.e-control-wrapper,
.e-spinner-pane .e-spinner-inner .e-spin-label,
.e-dashboardlayout.e-control .e-panel .e-panel-header {
    font-family: var(--ci-font);
}

/* ── Syncfusion cursors ───────────────────────────────────────────────────────────────────────────────────
   A PICKER'S BOX IS NOT A TEXT BOX. Syncfusion renders a dropdown's input as `readonly` — you choose from the
   list, you cannot type — and then styles it with a text I-beam anyway. Measured on one contract form: NINE of
   them, all saying "type here" over a box that ignores every keystroke, while the chevron half an inch to the
   right correctly said `pointer`. One control, two contradictory answers (pointer audit, 2026-09-06).

   Keyed on [readonly], which is exactly the condition that makes typing impossible, so it can never catch a box
   you CAN type in: SfTextBox, SfNumericTextBox and an editable date picker are all writable and keep their
   I-beam. A picker inside a disabled control is handled by the rule below and wins by coming after.
   Both the input and its wrapper are set — the wrapper is what the pointer is actually over between the text
   and the border. */
.e-input-group:has(> input.e-input[readonly]):not(.e-disabled),
.e-input-group input.e-input[readonly],
.e-multi-select-wrapper input[readonly] { cursor: pointer; }

/* Disabled, in Syncfusion's own vocabulary — the same answer our three button tiers now give. */
.e-input-group.e-disabled, .e-input-group.e-disabled input.e-input,
.e-control.e-disabled, .e-btn:disabled, .e-btn.e-disabled { cursor: not-allowed; }

/* ── Workflow designer form rows ──────────────────────────────────────────────────────────────────────────
   Shared by the three files that make up the workflow designer: the list's "New workflow" drawer
   (WorkflowsAdmin), a workflow's own page (WorkflowDetailPage) and everything inside it (WorkflowJobPanel,
   plus the four drawers it opens). They live here rather than in one of those pages' <style> blocks because
   a page's block only ships when THAT page renders — with the definitions on the list page, every one of
   these rows lost its layout the moment the panel moved to a page of its own. */
.wf-f { display: flex; align-items: flex-start; gap: .7rem; padding: .3rem 0; }
.wf-f > label:first-child { flex: 0 0 11rem; padding-top: .35rem; color: var(--ci-ink-2); font-size: .8rem; font-weight: 600; }
.wf-fv { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; flex: 1 1 auto; min-width: 0;
         color: var(--ci-ink-2); font-size: .8rem; }
.wf-fv-stack { flex-direction: column; align-items: flex-start; }
/* A stacked row is a COLUMN flex container, so .wf-in-wide's `flex: 1 1 18rem` basis stops being a width and
   becomes an 18rem HEIGHT — which is what turned the task editor's Process area picker into a tall empty box.
   Inside a stack the control sizes itself and takes the row's width instead. */
.wf-fv-stack > .wf-in, .wf-fv-stack > .wf-num { flex: 0 0 auto; width: 100%; max-width: 24rem; }
.wf-in { padding: .33rem .5rem; border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius-sm); font: inherit; font-size: .82rem;
         color: var(--ci-brand-ink); background: var(--ci-surface); min-width: 9rem; }
.wf-in:focus { outline: none; border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
.wf-in-wide { flex: 1 1 18rem; min-width: 13rem; }
/* The searchable people pickers (WorkflowJobPanel). Sized like .wf-in-wide, but the rule is scoped to the form
   row on purpose: Syncfusion puts CssClass on the POPUP as well as the control, and the popup is appended to the
   page body, where a flex-item width would mean nothing good. */
.wf-fv > .wf-picker { flex: 1 1 18rem; min-width: 13rem; max-width: 100%; }
/* And the stacked case, for the same reason .wf-in has one two rules up: in a COLUMN the 18rem basis above is a
   height, not a width, so the alert editor's "Send it to" picker would open as a tall empty box. */
.wf-fv-stack > .wf-picker { flex: 0 0 auto; width: 100%; max-width: 24rem; }
.wf-num { width: 4.5rem; padding: .33rem .5rem; border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius-sm); font: inherit;
          font-size: .82rem; }
.wf-note { width: 100%; min-height: 4.5rem; resize: vertical; }
/* The task Instructions editor. Wider than the 24rem the plain inputs cap at, because a toolbar of formatting
   buttons wraps onto three rows at that width and the strip stops reading as one control. */
.wf-rte { flex: 0 0 auto; width: 100%; max-width: 46rem; }
.wf-chk2 { display: inline-flex; align-items: center; gap: .4rem; font-size: .8rem; color: var(--ci-ink-2); cursor: pointer; }
.wf-checks { display: flex; flex-wrap: wrap; gap: .3rem 1rem; flex: 1 1 auto; padding-top: .35rem; }
.wf-hint { color: var(--ci-ink-faint); font-size: .74rem; }
.wf-hint-over { color: var(--ci-danger); font-weight: 600; }

/* ── Sub-table sheet (SubTableSheet) ─────────────────────────────────────────────────────────────────────────
   The many-rows-at-once editor behind a sub-grid's "Bulk add" and behind the saved starting rows that button can
   begin from. Shared, so the styles live here rather than in either host. */
.stb-sheet-wrap { overflow: auto; max-height: 60vh; border: 1px solid var(--ci-line); border-radius: var(--ci-radius); background: var(--ci-surface); }
.stb-sheet { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; }
.stb-sheet th, .stb-sheet td { padding: .35rem .5rem; border-bottom: 1px solid var(--ci-line); font-size: .85rem;
                               text-align: left; vertical-align: middle; }
.stb-sheet th { position: sticky; top: 0; z-index: 2; background: var(--ci-surface-sunk); color: var(--ci-brand-ink);
                font-weight: 600; white-space: nowrap; border-bottom: 1px solid var(--ci-line-strong); }
.stb-req { color: var(--ci-danger); margin-left: .15rem; }

/* The row number and the row tools stay put while the columns scroll — on a 26-column sheet, losing track of which
   line you are on is the whole problem. */
.stb-rowno { position: sticky; left: 0; z-index: 1; background: var(--ci-surface); color: var(--ci-ink-quiet);
             width: 2.4rem; text-align: right; font-variant-numeric: tabular-nums; }
.stb-sheet th.stb-rowno { z-index: 3; }
.stb-rowtools { position: sticky; right: 0; z-index: 1; background: var(--ci-surface); white-space: nowrap; }
.stb-sheet th.stb-rowtools { z-index: 3; }

.stb-sheet tbody tr:hover td { background: var(--ci-surface-2); }
.stb-sheet tbody tr.is-open td { background: var(--ci-surface-2); }
.stb-sheet tbody tr.is-failed .stb-rowno { background: var(--ci-danger-surface); color: var(--ci-danger); font-weight: 700; }

/* An unopened cell reads as text but keeps the row height an editor needs, so opening a row doesn't make the
   sheet jump. */
.stb-text { display: inline-block; min-height: 1.9rem; line-height: 1.9rem; width: 100%;
            overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ci-ink-body); }

/* COLUMN WIDTH IS THE WHOLE ERGONOMICS OF THIS SHEET. Left to themselves the Syncfusion editors take their natural
   width (~15rem each) and a 15-column sub-table shows three columns and a scrollbar. Every cell is clamped and every
   control inside it told to fill its cell instead of asking for room — legacy sized this grid's headers at a flat
   175px for the same reason. */
.stb-sheet td, .stb-sheet th { min-width: 6.5rem; max-width: 11rem; }
.stb-sheet .stb-rowno, .stb-sheet .stb-rowtools { min-width: 0; max-width: none; }
.stb-sheet td .e-control,
.stb-sheet td .e-input-group,
.stb-sheet td .field-input,
.stb-sheet td .e-multi-select-wrapper { width: 100% !important; min-width: 0 !important; }
/* The lookup ＋ button belongs on the record form, not in a cell — the row's own width is the scarce thing here. */
.stb-sheet td .field-addnew { display: none; }
.stb-sheet td .field-lookup-row { display: block; }
/* …and with the cap gone, give the picker its right corners back — the record form squares them off on
   `:has(> .field-addnew)`, which still matches a cap that is only display:none. */
.stb-sheet td .field-lookup-row > .field-lookup { border-radius: var(--ci-radius-sm); }
.stb-sheet td .e-input-group, .stb-sheet td .e-control-wrapper { margin: 0; }
.stb-sheet td .e-input-group input, .stb-sheet td input.e-input { padding-top: .2rem; padding-bottom: .2rem; }

/* ONE CONTROL TALL. The open row is as tall as its tallest cell, and the record form's editors are sized for a form:
   a memo three or more lines deep, a multi-select with room for chips, a slider with its scale and a readout under
   it. Any one of those made the open row 150px+ against ~32px closed rows (bug 48181 polish). In a cell each gets
   the height of a text box; a memo opens up only while it is being typed in. */
.stb-sheet td textarea.e-textbox { height: 1.95rem !important; min-height: 1.95rem !important; resize: none !important;
                                   line-height: 1.35; padding-top: .25rem; padding-bottom: .25rem; overflow-y: auto; }
.stb-sheet td textarea.e-textbox:focus { height: 6.5rem !important; }
.stb-sheet td .e-multi-line-input { padding-top: 0 !important; padding-bottom: 0 !important; }
.stb-sheet td .e-multi-line-input textarea.e-textbox { margin: 0 !important; }
.stb-sheet td .field-count { display: none; }
.stb-sheet td .e-multiselect, .stb-sheet td .e-multi-select-wrapper { min-height: 1.95rem !important; }
.stb-sheet td .e-multi-select-wrapper { max-height: 4rem; overflow-y: auto; }
/* The slider: scale off (the readout beside it already names the position), readout on the same line, and a track
   long enough to aim at — the cell grows for it rather than squeezing it to a stub. */
.stb-sheet td .field-slider-row { flex-direction: row; align-items: center; gap: .5rem; padding: 0 .35rem; }
.stb-sheet td .field-slider-row .e-slider-container { flex: 1 1 auto; min-width: 6.5rem; height: 1.95rem; }
.stb-sheet td .field-slider-row .e-scale { display: none; }
.stb-sheet td:has(.field-slider-row) { max-width: 14rem; }
.stb-sheet td .field-slider-readout { margin-top: 0; flex: none; font-size: .8rem; white-space: nowrap; }

.stb-off { background: var(--ci-surface-2); }
.stb-na { color: var(--ci-ink-disabled); }
.stb-flag { box-shadow: inset 0 0 0 2px #e57373; border-radius: var(--ci-radius-xs); }
.stb-msgrow td { border-bottom: 1px solid var(--ci-line); background: var(--ci-surface-2); }
.stb-msg { color: var(--ci-danger); font-size: .82rem; }

/* ── EXPANDABLE ROWS: THE ONE ADMIN ACCORDION ────────────────────────────────────────────────────────────────
   The shared look for a list whose rows open IN PLACE — the ratified U5 exception, used where a row's whole
   content is two paragraphs and a picture and a detail page would be a navigation for nothing (System Requests,
   both sides of it).

   It exists because every screen that needed one built its own. The workflow list's `.wf-card` recipe below is
   the original and the best of them, so it is what these names carry: a white card, a chevron on the LEFT that
   turns, a quiet hover, and a hairline rule between the head and the body. System Requests had drifted from it
   on the border colour, the row height, the gap between cards, and — the visible one — it put maximize/minimize
   glyphs on the RIGHT where every other accordion in the app puts a turning chevron on the left, so the two
   screens didn't read as the same control.

   A page still owns its row's CONTENT (which facts, in which order). What it must not own again is the frame. */
.ci-acc { display: flex; flex-direction: column; gap: .35rem; min-width: 0; }

.ci-acc-item { border: 1px solid var(--ci-line); border-radius: var(--ci-radius); background: var(--ci-surface); }
.ci-acc-item.is-open { border-color: var(--ci-line); box-shadow: 0 2px 10px rgba(44, 64, 112, .07); }
/* One flag colour, on the left edge rather than the whole row: an urgent request is not an error, it is next. */
.ci-acc-item.is-flagged { border-left: 3px solid var(--ci-danger-line); }

/* The whole heading is the expander, so the target is the size of the thing it opens. */
.ci-acc-head { width: 100%; display: flex; align-items: center; gap: .55rem; min-width: 0; padding: .5rem .7rem;
    background: transparent; border: 0; font: inherit; text-align: left; color: var(--ci-ink-body); cursor: pointer;
    border-radius: var(--ci-radius); transition: background var(--ci-transition); }
.ci-acc-head:hover { background: var(--ci-surface); }
.ci-acc-head:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: -2px; }

/* The chevron turns; it does not swap for a different glyph. Turning is what says "this is the same row, opened"
   — two different icons read as two different buttons. */
.ci-acc-chev { flex: none; display: inline-flex; color: var(--ci-ink-label); transition: transform var(--ci-transition); }
.ci-acc-item.is-open .ci-acc-chev { transform: rotate(90deg); color: var(--ci-accent-ink); }

/* NO "eyebrow" here on purpose. There was a `.ci-acc-eyebrow` — quiet caps naming the row's KIND, placed ahead
   of the subject — and it was wrong for the job: every row then opened with a category and the sentence you are
   actually scanning for started at a different x on every line. A row's kind belongs in a `.ci-badge` AFTER the
   title, inside the .ci-acc-main group below. */
/* THE ROW'S SUBJECT AND WHATEVER QUALIFIES IT, AS ONE GROUP. This wrapper is what takes the row's free space,
   so a state badge stays beside the name it describes and only the quiet meta is pushed to the right edge. It
   exists because the obvious alternative — letting the title itself grow — shoves the badges across the row and
   away from the name, and because the name has to be the part that ellipsises when the row is narrow. */
.ci-acc-main { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: .45rem; }
.ci-acc-title { flex: 0 1 auto; min-width: 0; color: var(--ci-brand-ink); font-weight: 700; font-size: .87rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Outside a .ci-acc-main the title still has to claim the row, or the meta sits against it. */
.ci-acc-head > .ci-acc-title { flex: 1 1 auto; }
.ci-acc-meta { flex: none; color: var(--ci-ink-faint); font-size: .78rem; white-space: nowrap; }
.ci-acc-ico { flex: none; display: inline-flex; color: var(--ci-ink-support); }
/* A row's tags and state pills are the app's `.ci-badge` (+ -ok / -warn / -off / -alert), NOT a private one.
   There was a `.ci-acc-badge` here for a day and it was a fork of an existing shared class — exactly what U2
   forbids. All a head has to add is that a badge must not stretch or shrink, wherever in the head it sits
   (directly, or inside a .ci-acc-main group). */
.ci-acc-head .ci-badge { flex: none; }

/* The chevron already rotates; the body it reveals used to appear with no cue at all, so an expander that opened a
   tall section looked like the page had jumped rather than unfolded. `ci-veil-in` is U12's shape for anything
   opening IN the page, and it is a fade rather than an unfold on purpose: Blazor renders this element only when the
   section is open, so there is no closed state in the DOM for a height to travel from. */
.ci-acc-body { border-top: 1px solid var(--ci-line-faint); padding: .7rem .8rem .8rem; display: flex; flex-direction: column;
    gap: .6rem; animation: ci-veil-in var(--ci-enter) ease-out; }

/* A row that carries actions BESIDE its expander (not inside it — a button nested in a button is invalid, and
   clicking it would also toggle the row). The head flexes, the actions sit at the right and keep their own clicks. */
.ci-acc-headrow { display: flex; align-items: center; gap: .3rem; padding-right: .5rem; }
.ci-acc-headrow > .ci-acc-head { flex: 1 1 auto; min-width: 0; }
.ci-acc-headrow > .ci-acc-acts { flex: none; display: flex; align-items: center; gap: .4rem; }

/* ── COLLAPSIBLE SECTIONS: THE ONE HEADING THAT FOLDS ────────────────────────────────────────────────────────
   The OTHER collapsible in this app, and a different control from `.ci-acc*` above: a heading bar that folds a
   whole block of the page (a group of admin cards, a phase of setup, one record type's workflows, a search's
   results for one object). `.ci-acc*` is a row IN a list; this is a heading OVER one.

   Five admin screens had hand-rolled it and every one advertised itself differently — AdminHub rotated a chevron,
   SessionsAdmin and ScheduledJobs SWAPPED the glyph, Launchpad and the health board used literal "▸"/"▾" text
   characters, and the counts were three different pills. Same class of drift the accordion had.

   The chevron TURNS here too, for the same reason it does above: one glyph, rotated, says "this is the same
   heading, opened". Two glyphs read as two different buttons.

   `.ci-sec` is the default scale — a real section heading over a block of content. `.ci-sec-caps` is the quiet
   uppercase variant for a page that is ALL sections and would otherwise shout (the admin hub, the launchpad). */
.ci-sec { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin: 1.3rem 0 .55rem; }
.ci-sec:first-child, .ci-sec-first { margin-top: 0; }

.ci-sec-head { display: flex; align-items: center; gap: .5rem; flex: 1 1 auto; min-width: 0; padding: .3rem .35rem .3rem 0;
    margin: 0; border: 0; background: transparent; font: inherit; text-align: left; color: var(--ci-brand-ink);
    cursor: pointer; border-radius: var(--ci-radius); }
.ci-sec-head:hover .ci-sec-t { color: var(--ci-accent-ink); }
.ci-sec-head:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 2px; }

.ci-sec-chev { flex: none; display: inline-flex; color: var(--ci-ink-faint); transition: transform var(--ci-transition); }
.ci-sec-head[aria-expanded="false"] .ci-sec-chev { transform: rotate(-90deg); }
.ci-sec-head:hover .ci-sec-chev { color: var(--ci-accent-ink); }

/* The title and, under it, the one line of context that says what folding this hides.
   `flex: 1 1 auto` so this column CLAIMS the width of the heading and the count pill after it lands on the right
   edge. Without it the column shrank to its widest child — which is the sub-line, not the title — so the pill
   stopped wherever that sentence happened to end: a different x on every section, reading as a number dropped
   mid-sentence rather than a count of the section. Down a folded page they now line up. */
.ci-sec-titles { display: flex; flex-direction: column; gap: .18rem; min-width: 0; flex: 1 1 auto; }
.ci-sec-t { color: var(--ci-brand-ink); font-size: var(--ci-fs-section); font-weight: var(--ci-fw-section);
    letter-spacing: var(--ci-ls-section); line-height: 1.2; transition: color var(--ci-transition); }
.ci-sec-sub { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; color: var(--ci-ink-support); font-size: .82rem;
    font-weight: 400; }
/* The count stays visible when the section is SHUT — closing a section must never hide whether it still needs you.
   It rides at the END of the heading (see .ci-sec-titles), not against the title text. */
.ci-sec-n { flex: none; margin-left: .75rem; padding: .05rem .5rem; border-radius: var(--ci-radius-pill); background: color-mix(in srgb, var(--ci-accent) 8%, var(--ci-surface)); color: var(--ci-brand-ink);
    font-size: .72rem; font-weight: 700; }
/* Actions belonging to the section, at the right of its heading. Outside the button, as in `.ci-acc-headrow`. */
.ci-sec-acts { flex: none; display: flex; align-items: center; gap: .5rem; margin-left: auto; }
/* A quiet fact pushed to the far right INSIDE the heading — "3/7", "no findings". The pill (.ci-sec-n) is for a
   bare count that belongs beside the title; this is for a phrase that belongs at the end of the bar. */
.ci-sec-tail { flex: none; margin-left: auto; color: var(--ci-ink-faint); font-size: .74rem; font-weight: 700;
    letter-spacing: 0; text-transform: none; white-space: nowrap; }

/* The quiet variant: a page made entirely of folding sections. Small caps, no rule, tighter. */
.ci-sec-caps { margin: 1.15rem 0 .5rem; }
/* Navy, not a grey: caps make it quieter already, and a section heading still has to out-rank the card titles
   underneath it. A dense rail of these (the launchpad's phases) overrides the colour locally. */
.ci-sec-caps .ci-sec-t { font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
.ci-sec-caps .ci-sec-head { padding: .25rem .3rem; border-radius: var(--ci-radius-sm); }
/* On a page that IS its sections, the bar itself lights up — there is no card edge to show the hit target. */
.ci-sec-caps .ci-sec-head:hover { background: var(--ci-surface-sunk); }
.ci-sec-caps .ci-sec-head:hover .ci-sec-t { color: var(--ci-accent-ink); }
.ci-sec-caps .ci-sec-n { background: var(--ci-hover-strong); }
/* Shut, the whole heading recedes — a finished phase or a folded group is worth a line, not attention. */
.ci-sec-caps .ci-sec-head[aria-expanded="false"] .ci-sec-t { color: var(--ci-ink-faint); }

/* A hairline under the heading, for a page where sections sit directly on the background with no card around
   them and the fold would otherwise be the only thing separating two lists. */
.ci-sec-ruled { border-bottom: 1px solid var(--ci-line); padding-bottom: .35rem; }
.ci-sec-ruled:hover { border-bottom-color: var(--ci-line-strong); }

/* ── A SYNCFUSION FIELD THAT FITS ITS CONTAINER ──────────────────────────────────────────────────────────────
   Syncfusion puts the class you pass as CssClass on the control's own WRAPPER, which already has the theme's
   border, padding and background. A page that then declares `width: 100%; padding: …; border: 1px solid …` on
   that class gets a second border drawn around the first AND — with no box-sizing — a control that is its
   container's full width PLUS its own padding, which is exactly how a dialog's fields ended up hanging over the
   right edge. These are the two hooks a page actually needs; everything else the theme already did.

   `.ci-field` fills its container. `.ci-field-inline` sizes to its content for a control sitting in a row of
   buttons, and matches `.ci-btn`'s 30px so a picker beside Save is not visibly taller than it. */
.ci-field, .ci-field .e-input-group, .ci-field.e-input-group, .ci-field.e-control-wrapper {
    width: 100%; box-sizing: border-box; min-width: 0; }
/* A single-line control is exactly as tall as a chrome button, so a picker and the Save beside it line up instead of
   the button standing 4px proud of it. Syncfusion's own default was already 32px on most controls; this pins the
   stragglers to the same number. `.ci-field-area` is multi-line and excluded — it has to be free to grow. */
.ci-field:not(.ci-field-area).e-input-group, .ci-field:not(.ci-field-area) .e-input-group,
.ci-field:not(.ci-field-area).e-control-wrapper {
    height: var(--ci-btn-h); min-height: var(--ci-btn-h); }
.ci-field:not(.ci-field-area).e-input-group input.e-input,
.ci-field:not(.ci-field-area) .e-input-group input.e-input {
    height: calc(var(--ci-btn-h) - 2px); padding-top: 0; padding-bottom: 0; }
.ci-field-inline { width: auto; min-width: 10rem; max-width: 100%; }
/* The compact tier: matches .ci-btn rather than .ci-hbtn, for a picker sitting in an in-body row.
   ★ box-sizing, which `.ci-field` gets from its own rule above and this one did NOT: without it the 30px is the
   CONTENT box and the control's own 1px border is added outside, so a "compact" field measured 32px beside a 30px
   .ci-btn. The whole point of U7 is one height per row, and this tier was quietly missing it by 2px everywhere it
   is used (measured 2026-09-18 on Package Manager's column search). */
.ci-field-inline.e-input-group, .ci-field-inline .e-input-group, .ci-field-inline.e-control-wrapper {
    height: var(--ci-btn-h-sm); min-height: var(--ci-btn-h-sm); box-sizing: border-box; }
.ci-field-inline.e-input-group input.e-input, .ci-field-inline .e-input-group input.e-input,
.ci-field-inline input.e-input {
    height: calc(var(--ci-btn-h-sm) - 2px); padding-top: 0; padding-bottom: 0; font-size: .8rem; }
/* Syncfusion also copies CssClass onto the POPUP, which is appended to <body> — where a 100% width would be the
   width of the page. The popup takes its width from the control it belongs to, so undo it there. */
.e-popup.ci-field, .e-popup.ci-field-inline { width: auto; }
/* A multi-line box needs a floor and the freedom to grow; the theme gives a text box one line and stops. */
.ci-field-area textarea.e-input { min-height: 4.5rem; resize: vertical; }

/* ── THE SEARCH STRIP INSIDE A FILTERING DROPDOWN ────────────────────────────────────────────────────────────
   `AllowFiltering="true"` makes Syncfusion prepend a bare <input> to the popup. Unstyled it renders as an empty
   box sitting flush under the control, the same width, with no border of its own, no icon and (unless the page
   passes FilterBarPlaceholder) no text — so it reads as a second, broken copy of the field rather than as
   "type to narrow this list". 30 screens turn filtering on and not one of them styles it, because the popup is
   appended to <body> where no page's own CSS can reach it. Hence here.

   Give it a strip of its own and put a real control inside it, at the compact tier: a search box in a popover is
   not the page's main field. The ::before/::after are the theme's floating-label underline, which has nothing to
   sit under here.

   NOTE: styling it does not give it words. A dropdown whose list is long enough to need filtering should also
   pass FilterBarPlaceholder ("Search tables"); an empty box with a nice border is still an unlabelled box. */
.e-ddl.e-popup .e-filter-parent {
    display: block; padding: .4rem .45rem; background: var(--ci-surface-2);
    border-bottom: 1px solid var(--ci-line); }
.e-ddl.e-popup .e-filter-parent .e-input-group {
    height: var(--ci-btn-h-sm); min-height: var(--ci-btn-h-sm); box-sizing: border-box;
    display: flex; align-items: center; margin: 0;
    border: 1px solid var(--ci-line-strong); border-radius: var(--ci-radius-sm);
    background: var(--ci-surface); box-shadow: none; }
.e-ddl.e-popup .e-filter-parent .e-input-group::before,
.e-ddl.e-popup .e-filter-parent .e-input-group::after { display: none; }
.e-ddl.e-popup .e-filter-parent .e-input-group.e-input-focus {
    border-color: var(--ci-accent); box-shadow: var(--ci-ring); }
.e-ddl.e-popup .e-filter-parent input.e-input-filter {
    height: calc(var(--ci-btn-h-sm) - 2px); padding: 0 .5rem; font-size: .82rem;
    background: transparent; color: var(--ci-ink-body); border: 0; }

/* ---------------------------------------------------------------------------------------------------------------
   Employee card (.ci-emp*) — the hover card behind any person named anywhere in the app.

   Global rather than a component .razor.css on purpose, and the reason matters: the card is rendered by
   CiEmployeeCard but POSITIONED by whoever hosts it (the picker anchors it beside the control, a name in a
   read-only field drops it underneath), and scoped isolation gives a child component's markup the CHILD's scope,
   so a host could never place it. It is also drawn next to a Syncfusion popup, which Syncfusion moves to the end
   of <body> — outside any component's scope. One shared family, per rule 7 / U2.

   Colour is theme variables throughout, so a client repainting the app repaints the card with it.
   --------------------------------------------------------------------------------------------------------------- */

/* ── A mention in a note (@somebody) ─────────────────────────────────────────────────────────────────────────────
   The chip lives INSIDE stored note HTML, so it is painted in three different places from this one rule: the rich
   editor while the note is being written, the read pane afterwards, and the note previews in the list. That is why
   it is here rather than in a component's scoped CSS — a .razor.css scope travels with the markup that declared it,
   and none of those three own this markup. A tint of the accent rather than a solid fill, because a note can name
   several people and a row of solid badges reads as buttons. */
.ci-mention {
    /* In a SAVED note the chip carries a hover card, so it points like every other card host. In the editor the
       caret still has to be able to sit next to it — that is the editor's own cursor on the text around it, which
       this doesn't touch. */
    cursor: pointer;
    background: color-mix(in srgb, var(--ci-accent) 12%, var(--ci-surface));
    border-radius: var(--ci-radius-xs);
    color: var(--ci-accent-ink);
    font-weight: 600;
    padding: 0 .18em;
    white-space: nowrap;
}

/* Syncfusion wraps whatever the mention's display template emits in its own non-editable chip. Ours carries the
   colour, so the wrapper must not add a second box around it inside the editor. */
.e-mention-chip { background: none; border: 0; padding: 0; }

/* The suggestion popup. It arrives with Syncfusion's own chrome — a saturated blue selection bar, its own greys and
   a taller row than anything else in the app — and it is appended to <body>, outside every component's scope, which
   is why it is themed HERE and not in RecordNotes. Selection reads as a tint plus the ink, the way every other list
   in this app shows the row you are on. */
.e-mention.e-popup {
    background: var(--ci-surface);
    border: 1px solid var(--ci-line-strong);
    border-radius: var(--ci-radius);
    box-shadow: var(--ci-shadow-lg);
    overflow: hidden;
}
.e-mention.e-popup .e-list-item {
    background: var(--ci-surface);
    color: var(--ci-ink-body);
    border: 0;
    line-height: 1.3;
    height: auto;
    min-height: 0;
    padding: .3rem .6rem;
}
.e-mention.e-popup .e-list-item.e-hover,
.e-mention.e-popup .e-list-item.e-active,
.e-mention.e-popup .e-list-item.e-active.e-hover {
    background: color-mix(in srgb, var(--ci-accent) 10%, var(--ci-surface));
    color: var(--ci-ink-body);
    border: 0;
}
.e-mention.e-popup .e-list-item + .e-list-item { border-top: 1px solid var(--ci-line-faint); }

/* ─── CiInfoTip — a caveat you can ask for ────────────────────────────────────────────────────────────────────────
   The glyph is a GHOST (U12): no resting box, the pill arrives on hover. It is the right call here and it does not
   trip the "a ghost is wrong for the only door" clause — the glyph is never the only way to anything, it holds no
   action, and a resting outline beside every label would read as a column of tiny buttons among the fields.

   The bubble is `position: fixed`, placed by ciPlaceInfoTip. See .ci-emp-card-fixed below for why absolute
   positioning cannot work here: an overflow ancestor CLIPS it and no z-index escapes a clip.

   Cursor is `help`, per U14 — the glyph explains itself and commits nothing. */
.ci-itip { position: relative; display: inline-flex; vertical-align: middle; }

.ci-itip-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.15rem; height: 1.15rem; padding: 0; flex: none;
    border: 0; border-radius: var(--ci-radius-pill); background: none;
    color: var(--ci-ink-quiet); cursor: help;
    transition: background var(--ci-transition), color var(--ci-transition);
}
.ci-itip-btn:hover, .ci-itip-btn[aria-expanded="true"] { background: var(--ci-hover); color: var(--ci-accent-ink); }
.ci-itip-btn:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 1px; }

.ci-itip-pop {
    position: fixed; top: 0; left: 0;
    z-index: var(--ci-z-dialog);   /* it can be asked for from inside a drawer or a dialog, so it has to clear them */
    display: block; width: 17rem; max-width: calc(100vw - 1rem);
    box-sizing: border-box; padding: .5rem .65rem;
    background: var(--ci-surface); border: 1px solid var(--ci-line);
    border-radius: var(--ci-radius); box-shadow: var(--ci-shadow-lg);
    font-size: .78rem; font-weight: 400; line-height: 1.45; color: var(--ci-ink-2);
    /* Hidden until ciPlaceInfoTip has the coordinates, so it is never seen in the window's top-left corner waiting
       for them — the render and the interop call are two separate circuit round trips. */
    visibility: hidden; animation: none;
}
.ci-itip-pop[data-ci-placed] { visibility: visible; animation: ci-pop-in var(--ci-enter) var(--ci-enter-ease); }
.ci-itip-h { display: block; font-weight: 700; color: var(--ci-brand-ink); margin-bottom: .15rem; }
.ci-itip-b { display: block; }
.ci-itip-b p { margin: 0 0 .35rem; }
.ci-itip-b p:last-child { margin-bottom: 0; }
.ci-itip-b strong { color: var(--ci-ink-body); }

@media (prefers-reduced-motion: reduce) {
    .ci-itip-pop[data-ci-placed] { animation: none; }
}

/* ─── CiAiNote — "VISDOM is AI and can make mistakes." ────────────────────────────────────────────────────────────
   Deliberately the quietest text on whatever it sits under. Meta size, meta ink, no rule, no fill, no icon: it is a
   STANDING caveat about the feature, not a warning about the answer above it, and the app's warning vocabulary
   (--ci-warn on --ci-warn-surface, which .vd-note and .ins-visdom-note use for "VISDOM Services aren't connected")
   would say something went wrong. Those sample-mode notes are transient and about THIS answer; this one is always
   there and about the feature, so the two have to look different where they stack on the same panel.

   Centred, and the LINE only ever lands on a chat (owner, 2026-09-22) — the site-wide assistant and the workbench's
   Ask VISDOM panel. Both are panels it spans the full width of, and left-aligning it under a centred send row read
   as a failed field hint. Everywhere else VISDOM appears takes .ci-ainote-tip below instead.

   The ink is --ci-ink-support, NOT the --ci-ink-meta that text this size normally takes. Meta ink (#8ea0c2) is tuned
   for a count or a timestamp sitting beside the thing it counts, where the reader already knows what it says; it
   lands at 2.6:1 on white, below AA at any size. This is a legal caveat, and a caveat nobody can read is worth
   nothing — support ink clears AA in both modes (4.8:1 light, 7.3:1 dark) and is still the quietest line on the
   panel. */
.ci-ainote {
    margin: 0; padding: .3rem .7rem .4rem; text-align: center;
    font-size: var(--ci-fs-meta); font-weight: 400; line-height: 1.35; color: var(--ci-ink-support);
}

/* The tip form carries almost no layout of its own — it is a glyph borrowed from CiInfoTip and dropped into somebody
   else's heading, so all it owns is the hair of space after the word it follows, and not pushing that heading's
   baseline around. It inherits nothing from the heading it lands in: CiInfoTip's button sets its own colour, which
   is what keeps the glyph quiet inside an h1, an h2, a caps section head and a count line alike. */
.ci-ainote-tip { display: inline-flex; align-items: center; vertical-align: middle; margin-left: .3rem; }

/* On the page header it follows a 1.5rem title that already spaces its own badges, so it needs less, not more. */
.ci-phead-title .ci-ainote-tip { margin-left: .1rem; }

/* DRAFT WITH VISDOM — the small pill at the right-hand end of a field's label row that fills the box under it with a
   first draft (help text, a business justification, negotiating guidance). Lifted from the clause screen's .cx-ai so
   every Draft in the product is the same object; the AI ink marks it as VISDOM's, the pill shape keeps it from
   reading as the form's commit. Pair it with .ci-label-row so the label and the pill share a baseline. */
.ci-label-row { display: flex; align-items: center; gap: .4rem; min-height: 1.4rem; }
.ci-ai-draft {
    margin-left: auto; display: inline-flex; align-items: center; gap: .25rem; cursor: pointer;
    padding: .12rem .5rem; border-radius: var(--ci-radius-pill); font: inherit; font-size: .72rem; font-weight: 700;
    border: 1px solid var(--ci-line-strong); background: var(--ci-surface-sunk); color: var(--ci-ai-ink);
}
.ci-ai-draft:hover:not(:disabled) { background: var(--ci-hover-strong); }
.ci-ai-draft:disabled { opacity: .6; cursor: default; }

/* ── THE REVIEW DRAWER: A DOCUMENT BESIDE THE RECORD IT BECOMES ──────────────────────────────────────────────────
   The intake agent's review drawer (IntakeReviewDrawer, .ikd-*) set this shape and the owner refined it over several
   rounds; Launchpad's Records drawer (SetupRecordDrawer) is a separate drawer that must FEEL the same (owner,
   2026-10-15: "separate drawers but a similar and familiar user experience"). So the shape lives here, at intake's own
   measurements, rather than in a second private copy. Intake still carries its .ikd-* copy; moving it onto these is
   the follow-up that makes this the only one.

   Anatomy: the pages on the left (sticky, scrolling on their own), the record on the right as ONE line per field -
   label | value | page pin - grouped as "missing required" (boxed, danger stripe), "Filled in", and a folded "Other
   fields". A value VISDOM read carries the sparkles pin with its page; clicking it outlines the value on the page. */
.ci-rv { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 400px); gap: .6rem; align-items: start; }
.ci-rv-doc { background: var(--ci-surface-2); border: 1px solid var(--ci-line); border-radius: var(--ci-radius); padding: .6rem;
             min-height: 240px; position: sticky; top: 0; max-height: calc(100vh - 10.5rem); overflow: auto; }
.ci-rv-doctools { display: flex; align-items: center; gap: .5rem; font-size: .74rem; color: var(--ci-ink-quiet); margin-bottom: .5rem; flex-wrap: wrap; }
.ci-rv-doctools-right { margin-left: auto; display: inline-flex; gap: .75rem; align-items: center; }
.ci-rv-doctools-focus { color: var(--ci-ai-ink, var(--ci-ai)); font-weight: 600; }
.ci-rv-link { color: var(--ci-accent); font-size: .78rem; text-decoration: none; }
.ci-rv-link:hover { text-decoration: underline; }
.ci-rv-pages { display: grid; gap: .6rem; }
.ci-rv-pagewrap { position: relative; line-height: 0; }
/* A page is paper whatever the theme. */
.ci-rv-page { width: 100%; height: auto; background: #fff; border: 1px solid var(--ci-line); box-shadow: var(--ci-shadow-sm); display: block; }
/* A value, drawn where it sits: fractions of the page as inline geometry, the look here. One at a time, only once a
   value has been picked - a page opening with every read value outlined read as everything being selected. */
.ci-rv-hl { position: absolute; pointer-events: none; border-radius: 2px;
            background: color-mix(in srgb, var(--ci-ai) 30%, transparent);
            outline: 2px solid color-mix(in srgb, var(--ci-ai) 75%, transparent); outline-offset: 1px; }

.ci-rv-side { background: var(--ci-surface); border: 1px solid var(--ci-line); border-radius: var(--ci-radius); overflow: hidden; }
.ci-rv-alert { margin: .5rem .75rem; }
.ci-rv-block { padding: .5rem .75rem .6rem; border-bottom: 1px solid var(--ci-line); }
.ci-rv-block-t { font-size: .84rem; font-weight: 700; }
.ci-rv-block-d { font-size: .74rem; color: var(--ci-ink-quiet); margin: .1rem 0 .45rem; }

/* Group headings: quiet caps on a band, with the count as a pill. The fold is a group heading that presses. */
.ci-rv-grp { display: flex; align-items: center; gap: .4rem; width: 100%; box-sizing: border-box; padding: .3rem .75rem;
             font: inherit; font-size: .66rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; text-align: left;
             color: var(--ci-ink-quiet); background: var(--ci-surface-2); border: 0; border-top: 1px solid var(--ci-line);
             border-bottom: 1px solid var(--ci-line); }
.ci-rv-grp-n { font-size: .64rem; font-weight: 700; border-radius: var(--ci-radius-pill); padding: 0 .4rem; background: var(--ci-surface);
               border: 1px solid var(--ci-line); color: inherit; letter-spacing: 0; }
.ci-rv-grp-btn { cursor: pointer; }
.ci-rv-grp-btn:hover { background: var(--ci-hover); color: var(--ci-ink-2); }
.ci-rv-grp-btn:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: -2px; }
.ci-rv-grp-caret { margin-left: auto; display: inline-flex; transition: transform .15s ease; }
.ci-rv-grp-caret.is-open { transform: rotate(180deg); }
/* A section's name inside the folded rest, for a form long enough to have several. Quieter than a group. */
.ci-rv-sub { padding: .35rem .75rem .15rem; font-size: .66rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
             color: var(--ci-ink-faint); }
/* What stands between the document and its record: the colour the required marks use and a stripe down its edge, so
   it cannot be read as one more group of values. */
.ci-rv-todo { box-shadow: inset 3px 0 0 var(--ci-danger); }
.ci-rv-grp.ci-rv-todo-h { color: var(--ci-danger); background: var(--ci-danger-surface); border-color: var(--ci-danger-line); }

/* One line per field: label | value | page pin. A field being edited takes the full width under its label. */
.ci-rv-fr { display: grid; grid-template-columns: minmax(0, 36%) minmax(0, 1fr) auto; column-gap: .6rem; align-items: center;
            padding: .3rem .75rem; min-height: 2rem; box-sizing: border-box; border-bottom: 1px solid var(--ci-line-faint, var(--ci-line)); }
.ci-rv-fr:last-child { border-bottom: 0; }
.ci-rv-fr.is-focus { box-shadow: inset 3px 0 0 var(--ci-ai); }
.ci-rv-fr.can-locate { cursor: pointer; }
.ci-rv-fl { grid-column: 1; grid-row: 1; font-size: .74rem; color: var(--ci-ink-quiet); font-weight: 600; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
.ci-rv-fbody { grid-column: 2; grid-row: 1; min-width: 0; }
.ci-rv-fpin { grid-column: 3; grid-row: 1; display: inline-flex; }
.ci-rv-fr.is-editing .ci-rv-fbody { grid-column: 1 / -1; grid-row: 2; margin-top: .25rem; }
.ci-rv-fbody .e-input-group, .ci-rv-fbody .e-ddl.e-input-group { width: 100% !important; }
/* The AI marker: VISDOM read this, from this page. Pressable when the page is on screen; the hover says how sure. */
.ci-rv-pg { font: inherit; font-size: .66rem; font-weight: 600; color: var(--ci-ai-ink, var(--ci-ai)); background: var(--ci-ai-soft);
            border: 1px solid var(--ci-ai-line, var(--ci-line)); border-radius: var(--ci-radius-pill); padding: 0 .4rem; cursor: pointer;
            white-space: nowrap; display: inline-flex; align-items: center; gap: .15rem; }
.ci-rv-pg:hover { filter: brightness(.97); }
.ci-rv-pg.is-on { background: var(--ci-ai); color: var(--ci-ink-on-brand); border-color: var(--ci-ai); }
.ci-rv-pg.is-static { cursor: default; }
.ci-rv-pg.is-static:hover { filter: none; }
/* VISDOM was less sure: the same pin, in the warning colour. */
.ci-rv-pg.is-shaky:not(.is-on) { color: var(--ci-warn); background: var(--ci-warn-surface); border-color: var(--ci-warn-line); }
.ci-rv-fv { font-size: .84rem; font-weight: 500; word-break: break-word; border-radius: var(--ci-radius-sm); padding: .05rem .2rem;
            margin-left: -.2rem; display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.ci-rv-fv.can-edit { cursor: text; }
.ci-rv-fv.can-edit:hover { background: var(--ci-hover); outline: 1px dashed var(--ci-ctl-border); }
.ci-rv-fs { font-size: .68rem; color: var(--ci-ink-quiet); line-height: 1.3; }
/* An empty field looks like an empty field: a box to fill. */
.ci-rv-blank { display: flex; align-items: center; justify-content: space-between; gap: .4rem; width: 100%; min-height: 1.6rem;
               box-sizing: border-box; font: inherit; font-size: .8rem; color: var(--ci-ink-quiet); text-align: left; cursor: text;
               background: var(--ci-ctl-bg); border: 1px solid var(--ci-ctl-border); border-radius: var(--ci-radius-sm); padding: .1rem .45rem; }
.ci-rv-blank:hover { border-color: var(--ci-accent); }
.ci-rv-blank.is-req { border-color: var(--ci-danger-line); }
.ci-rv-blank.is-req:hover { border-color: var(--ci-danger); }
.ci-rv-blank:disabled { cursor: default; opacity: .6; }
/* A one-line follow-up under a value ("Use on 3 other documents"). */
.ci-rv-act { display: inline-flex; margin-top: .15rem; padding: 0; border: 0; background: none; font: inherit; font-size: .72rem;
             font-weight: 600; cursor: pointer; color: var(--ci-accent-ink); }
.ci-rv-act:hover:not(:disabled) { text-decoration: underline; }
.ci-rv-act:disabled { cursor: default; opacity: .6; }

/* The footer's left edge: the position in the batch, then why the commit will not press - in the warning colour and a
   press away from the thing to do. A quiet grey note there was missed in a demo. */
.ci-rv-foot-start { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; }
.ci-rv-foot-todo { display: inline-flex; align-items: center; gap: .35rem; font: inherit; font-size: .78rem; font-weight: 600;
                   color: var(--ci-danger); background: none; border: 0; padding: .2rem 0; cursor: pointer; text-align: left; }
.ci-rv-foot-todo:hover span { text-decoration: underline; }
.ci-rv-foot-note { font-size: .76rem; color: var(--ci-ink-quiet); }
.ci-rv-nav { display: inline-flex; align-items: center; gap: .3rem; }
/* Square at the footer's button height, so the pair lines up with the buttons at the right. */
.ci-rv-navbtn { width: var(--ci-btn-h-sm); padding: 0; display: inline-flex; align-items: center; justify-content: center; }
.ci-rv-navpos { font-size: .74rem; color: var(--ci-ink-quiet); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Shrunk, the drawer is too narrow for two panes, so it is the fields alone. */
.ci-drawer:not(.is-wide) .ci-rv { grid-template-columns: minmax(0, 1fr); }
.ci-drawer:not(.is-wide) .ci-rv-doc { display: none; }
@media (max-width: 980px) {
    .ci-rv { grid-template-columns: minmax(0, 1fr); }
    .ci-rv-side { order: -1; }
    .ci-rv-doc { position: static; max-height: none; }
}

/* The positioning context for the card. Inline-block so it doesn't stretch a table cell or a field row. */
.ci-emp { position: relative; display: inline-block; }

.ci-emp-card {
    position: absolute;
    /* Above Syncfusion's popup: the card is drawn next to an OPEN dropdown and must not slide behind it. */
    z-index: 10010;
    width: 17.5rem;
    padding: .85rem .95rem;
    border: 1px solid var(--ci-line, var(--ci-line-strong));
    border-radius: var(--ci-radius-lg);
    background: var(--ci-surface-1, var(--ci-surface));
    box-shadow: 0 8px 28px rgba(44, 64, 112, .18);
    animation: ci-emp-in .12s ease-out;
    /* It TAKES pointer events, because it carries actions — which is also why each host keeps it alive briefly
       after the pointer leaves, so it can be reached. */
}

/* Placement is the host's choice, and each host sets one of these two.

   -right: beside the control, for a picker — the picker's own popup opens downward, so a card underneath would
   sit behind it.
   -below: under the text, for a name in a read-only field, where nothing opens downward and dropping it below
   keeps it near what was hovered.

   Each carries a matching invisible ::before pad bridging the gap to its host, so the pointer never crosses dead
   space on its way to the card. Without it, that few millimetres of nothing counts as leaving and the card closes
   under the pointer. */
.ci-emp-card-right { top: 0; left: calc(100% + .6rem); }
.ci-emp-card-right::before { content: ""; position: absolute; top: 0; bottom: 0; left: -.7rem; width: .7rem; }

.ci-emp-card-below { top: calc(100% + .4rem); left: 0; }
.ci-emp-card-below::before { content: ""; position: absolute; left: 0; right: 0; top: -.5rem; height: .5rem; }

/* -fixed: the same card, lifted OUT of the page's flow and placed by ciPlaceEmpCard.

   This exists because of a bug that LOOKS like a z-index bug and isn't. The card is drawn inside whatever renders
   the name, and a read-only employee field sits several containers deep — the stage cards, `.add-compact-sect` and
   the scrolling detail panels are all `overflow: hidden` or `overflow-y: auto`. An absolutely-positioned child is
   CLIPPED by those, and no z-index escapes a clip; raising it just moves a card that is still cut off. Fixed
   leaves the flow, so the only thing bounding the card is the window. CiRowActionMenu does exactly this to escape
   a grid's clipping — same problem, same answer.

   Coordinates come from JS, so it starts HIDDEN and ciPlaceEmpCard reveals it once they are set. That is not
   belt-and-braces: this is Blazor Server, so the render that creates the card and the interop call that places it are
   two separate round trips. Visible in between, the card paints in the top-left corner of the window — fading in
   there, because the entry animation would have started too — and then jumps under the name. Hence also the
   animation moving to the revealed state: it has to play where the card lands, not where it was parked.
   `visibility`, not `display`, because the helper measures the card's height before deciding which side to open on. */
.ci-emp-card-fixed { position: fixed; top: 0; left: 0; margin: 0; visibility: hidden; animation: none; }
.ci-emp-card-fixed[data-ci-placed] { visibility: visible; animation: ci-emp-in .12s ease-out; }
.ci-emp-card-fixed::before { content: ""; position: absolute; left: 0; right: 0; top: -.5rem; height: .5rem; }

@keyframes ci-emp-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }

/* The -fixed selector is listed too, and has to be: it is more specific than .ci-emp-card, so without it the
   revealed card would keep its animation here. */
@media (prefers-reduced-motion: reduce) {
    .ci-emp-card, .ci-emp-card-fixed[data-ci-placed] { animation: none; }
}

/* On a narrow viewport there is no room beside anything, so a right-anchored card drops below and spans instead. */
@media (max-width: 900px) {
    .ci-emp-card-right { left: 0; top: calc(100% + .4rem); width: min(20rem, 92vw); }
    .ci-emp-card-right::before { left: 0; right: 0; top: -.5rem; bottom: auto; width: auto; height: .5rem; }
}

.ci-emp-card-top { display: flex; align-items: center; gap: .7rem; margin-bottom: .6rem; }

/* The initials sit UNDER the photo: the image is painted on top and hides itself on a 404, so somebody with no
   photo still gets a filled avatar rather than a broken-image box. */
.ci-emp-avatar {
    position: relative; flex: none; width: 3rem; height: 3rem; border-radius: 50%; overflow: hidden;
    background: var(--ci-navy, var(--ci-navy)); display: flex; align-items: center; justify-content: center;
}
.ci-emp-initials { font-size: .95rem; font-weight: 700; color: var(--ci-ink-on-brand); letter-spacing: .02em; }
.ci-emp-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

.ci-emp-who { min-width: 0; }
/* Long names wrap rather than being cut — the name is the whole point of the card. */
.ci-emp-name { font-weight: 700; font-size: .9rem; color: var(--ci-ink, var(--ci-ink-body)); line-height: 1.25; overflow-wrap: anywhere; }
.ci-emp-dept { font-size: .76rem; color: var(--ci-ink-2, var(--ci-ink-2)); margin-top: .1rem; }

/* "Viewing this record" / "Editing this record" — the live line, and the place the presence stack's faces finally
   explain themselves in words. Opt-in per host (see CiEmployeeCard.StatusNote): every other card in the app shows
   nothing here, because a name in a read-only field has no "right now" to report.

   Green reads as present, amber as in-the-form, matching the dot on the avatar the card was opened from — the same
   distinction the user just hovered, said twice so the card confirms rather than introduces it. */
.ci-emp-status {
    display: inline-flex; align-items: center; gap: .3rem;
    margin-top: .25rem;
    font-size: .72rem; font-weight: 600; letter-spacing: .01em;
    color: var(--ci-ok);
}
.ci-emp-status.is-alert { color: var(--ci-warn); }
.ci-emp-status-dot {
    flex: none; width: .4rem; height: .4rem; border-radius: var(--ci-radius-pill);
    background: currentColor;
}

/* Label/value pairs as a two-column grid so the values line up regardless of label length. */
.ci-emp-facts { display: grid; grid-template-columns: 4.2rem 1fr; gap: .2rem .5rem; margin: 0; font-size: .78rem; }
.ci-emp-facts dt {
    color: var(--ci-ink-meta, var(--ci-ink-label)); font-size: .7rem; text-transform: uppercase; letter-spacing: .04em;
    font-weight: 700; align-self: center;
}
/* overflow-wrap, not word-break: it breaks a long value only when it has to. The card's own markup puts a <wbr>
   at an email's '@' so that break is preferred over one mid-domain; this stays as the fallback. */
.ci-emp-facts dd { margin: 0; color: var(--ci-ink, var(--ci-ink-body)); min-width: 0; overflow-wrap: anywhere; }
.ci-emp-facts a { color: var(--ci-accent, var(--ci-accent-ink)); text-decoration: none; }
.ci-emp-facts a:hover { text-decoration: underline; }

.ci-emp-thin { margin: 0; font-size: .76rem; color: var(--ci-ink-meta, var(--ci-ink-label)); font-style: italic; }

/* The COMPANY card (CiVendorCard) reuses the shell above — same geometry, same shadow, and above all the same fixed
   placement, so it cannot be clipped either. These are only the parts that differ, because it answers a different
   question: a person's card leads with contact details, a company's leads with how much we do with them.

   (The shell's classes are named .ci-emp-* and now serve both cards. Worth renaming to something neutral; not worth
   doing in a release week, and not while it would touch CiEmployeePicker.) */

/* A building glyph rather than initials or a photo — the different mark is what says "company, not person" before
   any of the text is read. Squared off for the same reason: the employee avatar is a circle. */
.ci-vend-avatar {
    background: var(--ci-navy, var(--ci-navy)); color: var(--ci-ink-on-brand);
    border-radius: var(--ci-radius-sm); display: grid; place-items: center;
}

/* The always-present line. Sits between the name and the facts because on most companies it is the only thing on the
   card with anything in it. */
.ci-vend-stats {
    display: flex; flex-wrap: wrap; gap: .15rem .7rem;
    margin: .5rem 0 .55rem; padding-top: .5rem;
    border-top: 1px solid var(--ci-line, var(--ci-line-strong));
}
.ci-vend-stat {
    display: inline-flex; align-items: center; gap: .3rem;
    font-size: .78rem; font-weight: 600; color: var(--ci-ink, var(--ci-ink-body));
}
.ci-vend-stat svg { color: var(--ci-accent, var(--ci-accent-ink)); flex: none; }

/* Actions as chips rather than a button row: they sit inside a hover card that is mostly information, where a
   full-height button row would dominate it. Wraps, because four chips don't fit 17.5rem on one line. */
.ci-emp-acts {
    display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .7rem; padding-top: .6rem;
    border-top: 1px solid var(--ci-line, var(--ci-line-strong));
}
.ci-emp-act {
    font: inherit; font-size: .74rem; font-weight: 600; line-height: 1.6; padding: 0 .55rem;
    border: 1px solid var(--ci-line, var(--ci-line-strong)); border-radius: var(--ci-radius-pill); background: var(--ci-surface, var(--ci-surface-sunk));
    color: var(--ci-accent, var(--ci-accent-ink)); text-decoration: none; cursor: pointer; white-space: nowrap;
}
.ci-emp-act:hover { border-color: var(--ci-accent, var(--ci-accent)); background: var(--ci-surface-1, var(--ci-surface)); }
.ci-emp-act:focus-visible { outline: 2px solid var(--ci-accent, var(--ci-accent)); outline-offset: 1px; }
/* The way OUT reads as the quiet one — it navigates away, so it shouldn't compete with contacting. */
.ci-emp-act-go { margin-left: auto; color: var(--ci-ink-2, var(--ci-ink-2)); }

/* A person's name rendered as text, in a read-only field or a table cell. Underlined faintly on hover so it reads
   as "there is more here" without looking like a link that navigates. */
/* Pointer, not `help` and not the text caret: there IS something here to interact with — the card has Email, Copy
   email and Open record on it — and a caret says the opposite, that this is text to select. Shared by every hover
   card host (CiEmployeeName, CiVendorName), so employee names, vendor names and mentions all say the same thing. */
.ci-emp-hover { cursor: pointer; text-decoration: underline dotted transparent; text-underline-offset: 3px; }
.ci-emp-hover:hover { text-decoration-color: var(--ci-ink-meta, var(--ci-ink-label)); }
/* The name is now a real anchor (CiEmployeeName/CiVendorName) — keep the NAME's ink, not the browser's link blue:
   the dotted hover underline + cursor stay its affordance, and the card still opens on hover. */
a.ci-emp-hover { color: inherit; }

/* A drill-in link inside a plain preview table (the dashboard report embeds): accent ink, underline on hover only —
   a fully-underlined first column would out-shout the data it identifies. */
.ci-cell-link { color: var(--ci-accent-ink); text-decoration: none; }
.ci-cell-link:hover { text-decoration: underline; }

/* "Somebody else has this record open" — the quietest badge in the app on purpose. It is a hint you benefit from
   noticing, not information the page is built around, so it reads as chrome: muted, an icon and a number, names on
   hover. Sits in the record title's badge row next to the record id.

   It is deliberately NOT coloured as a warning. Nothing is wrong when a colleague is reading the same contract, and
   an amber badge would send people looking for a problem that isn't there. */
/* Who has this record open, as overlapping faces. Replaced an eye-and-a-count: the count said how many, the faces
   say WHO — and the stack is a known idiom (Teams, Google Docs), so it needs no label to be read. */
.ci-pstack { display: inline-flex; align-items: center; vertical-align: middle; }

/* The overlap. Applied to the wrapper CiEmployeeName renders, since that is what sits in the flow.

   The z-index ladder is load-bearing, not decoration. Siblings paint in document order, so by default each avatar
   covers the RIGHT EDGE of the one before it — which ate both the previous face's initials and, worse, its live dot,
   leaving a stack of three that showed exactly one dot. Reversing the order so the leftmost paints on top puts every
   dot and every set of initials in front of its neighbour. Capped at three avatars (MaxAvatars), so three rungs. */
.ci-pstack .ci-emp { position: relative; margin-left: -.28rem; }
.ci-pstack .ci-emp:first-child { margin-left: 0; }

/* The ladder goes on the AVATAR, never on .ci-emp — and that distinction is the whole point.
   .ci-emp is the hover card's ANCESTOR, so a z-index there makes it a stacking context and traps the card inside it:
   the card's 10010 would then only rank against that one avatar's siblings, and a card opened from the third face
   would be painted UNDER the first two. The avatar is a leaf, so ordering it costs nothing and confines nothing.
   (Straight out of the z-index rule: fix the ancestor — or in this case, don't create one.) */
.ci-pstack .ci-emp:nth-child(1) .ci-pstack-av { z-index: 3; }
.ci-pstack .ci-emp:nth-child(2) .ci-pstack-av { z-index: 2; }
.ci-pstack .ci-emp:nth-child(3) .ci-pstack-av { z-index: 1; }

/* An avatar is not prose: strip the name-underline the hover target carries everywhere else. */
.ci-pstack .ci-emp-hover { text-decoration: none; border-bottom: 0; }

.ci-pstack-av {
    position: relative;
    display: grid; place-items: center;
    /* Exactly as tall as the .ci-badge pill it sits beside, written as that pill's OWN arithmetic — its .68rem line
       box, .25rem of padding top and bottom, and a 1px border each side — rather than a magic 21px, so the two do
       not drift apart when either is retuned. --ci-pstack-size is here so the live dot and the +N chip can share it. */
    --ci-pstack-size: calc(.68rem + .5rem + 2px);
    width: var(--ci-pstack-size); height: var(--ci-pstack-size);
    border-radius: var(--ci-radius-pill);
    background: var(--ci-navy, var(--ci-navy)); color: var(--ci-ink-on-brand);
    font-size: .55rem; font-weight: 700; letter-spacing: .01em;
    /* The ring is what separates one face from the next where they overlap, so it is the PAGE colour rather than a
       border colour — it has to read as the gap between them, not as an outline around each. */
    box-shadow: 0 0 0 2px var(--ci-content-bg, #f4f9fc);
    cursor: default;
}
.ci-pstack .ci-emp-hover:hover .ci-pstack-av { filter: brightness(1.15); }

/* The photo covers the initials edge-to-edge and removes itself on a 404 (the markup's onerror), leaving the
   initials showing. Rounded on the img itself rather than overflow:hidden on the avatar, because the live dot
   sits OUTSIDE the circle and clipping the avatar would cut the dot off; the dot follows the img in the markup,
   so it still paints on top of the photo. */
.ci-pstack-av img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; border-radius: var(--ci-radius-pill);
}

/* The live dot. Without it a row of faces reads as "people involved with this record" — permanent data — instead of
   "people here right now". Green because that is what every other product means by present. */
.ci-pstack-live {
    position: absolute; right: -1px; bottom: -1px;
    width: .36rem; height: .36rem;
    border-radius: var(--ci-radius-pill);
    background: #16a34a;
    box-shadow: 0 0 0 1.5px var(--ci-content-bg, #f4f9fc);
}

/* Only in a crowd: the names that didn't fit, behind a disclosure. */
.ci-pstack-more {
    margin-left: -.3rem;
    display: grid; place-items: center;
    /* Same box as an avatar, and it may grow WIDER for a three-digit overflow without growing taller. */
    --ci-pstack-size: calc(.68rem + .5rem + 2px);
    min-width: var(--ci-pstack-size); height: var(--ci-pstack-size); padding: 0 .22rem;
    border: 0; border-radius: var(--ci-radius-pill);
    background: var(--ci-ctl-bg, var(--ci-ctl-bg)); color: var(--ci-ink, var(--ci-ink-body));
    font: inherit; font-size: .58rem; font-weight: 700;
    box-shadow: 0 0 0 2px var(--ci-content-bg, #f4f9fc);
    cursor: pointer;
}
.ci-pstack-more:hover, .ci-pstack-more:focus-visible { background: var(--ci-line, var(--ci-hover-strong)); }

/* Who is here. Fixed and placed by ciPlaceEmpCard for the same reason the hover cards are: the badge sits in a
   record header several containers deep, and an absolutely-positioned panel gets clipped by one of them.

   Below .ci-emp-card's 10010 on purpose — a row in here IS an employee name, so its card has to be able to open
   OVER this panel rather than behind it. */
.ci-presence-pop {
    /* Hidden until ciPlaceEmpCard has the coordinates, for the reason spelled out at .ci-emp-card-fixed: the
       creating render and the placing interop call are two separate round trips, and in between this paints in the
       corner of the window. */
    position: fixed; top: 0; left: 0; z-index: 10005; visibility: hidden;
    min-width: 12rem; max-width: 17rem;
    padding: .5rem .55rem;
    border: 1px solid var(--ci-line, var(--ci-line-strong));
    border-radius: var(--ci-radius);
    background: var(--ci-surface-1, var(--ci-surface));
    box-shadow: var(--ci-shadow-lg);
}
.ci-presence-pop[data-ci-placed] { visibility: visible; }

/* The invisible pad bridging the gap up to the badge, so the pointer never crosses dead space on its way here. */
.ci-presence-pop::before { content: ""; position: absolute; left: 0; right: 0; top: -.5rem; height: .5rem; }

.ci-presence-pop-h {
    font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; font-weight: 700;
    color: var(--ci-ink-meta, var(--ci-ink-label));
    margin-bottom: .35rem;
}
/* Rows are names, not mini-cards: the full card is one hover deeper on any of them. */
.ci-presence-row {
    font-size: .82rem; padding: .12rem 0;
    /* The name takes the room it needs and the state word pins to the right, so the states line up down the panel
       and can be read as a column instead of hunted for at the end of each name. */
    display: flex; align-items: baseline; gap: .5rem;
}
.ci-presence-row .ci-emp { flex: 1 1 auto; min-width: 0; }
.ci-presence-state {
    flex: 0 0 auto;
    font-size: .68rem; font-weight: 600; letter-spacing: .02em;
    color: var(--ci-ink-quiet);
}
.ci-presence-state.is-editing { color: var(--ci-warn); }

/* ── "why is that photo there" ───────────────────────────────────────────────────────────────────────────────────
   The faces alone were reported as unreadable by the owner (2026-09-11): a photo beside a contract's title looks
   like somebody ASSIGNED to the record, which is exactly the confusion the live dot was meant to prevent and
   evidently doesn't.

   The answer IN WORDS lives on the hover card (.ci-emp-status) and in the +N panel's rows, not in the header. A
   running "1 viewing" label beside the record's title was built and removed the same day, on the owner's call: the
   title line is the most valuable real estate on the page and a live count was competing with the record's own name
   for it. The header keeps only what a glance can use — the face, and its colour.

   VIEWING vs EDITING is one distinction painted in two places: the amber ring + amber dot here, and the card's word.
   It is a hint with the same reach as the presence it rides on: it can under-report (another app instance, a
   just-closed tab) and it never over-reports, so it may inform a decision and must never gate one. */

/* The editing ring replaces the page-coloured separator ring rather than stacking a second one outside it — two
   rings on a 21px circle is a target, not an avatar. The inner page-colour rung is kept so the faces still read as
   separate where they overlap. */
.ci-pstack-av.is-editing {
    box-shadow: 0 0 0 2px var(--ci-content-bg, #f4f9fc), 0 0 0 3.5px var(--ci-warn);
}
.ci-pstack-av.is-editing .ci-pstack-live { background: var(--ci-warn); }

/* ── Google Places Autocomplete (the address lookup on Add Vendor / Customer) ─────────────────────────────────────
   Google appends its suggestion list to <body> as `div.pac-container` and hard-codes `z-index: 1000` on it. Every
   place we USE it is inside a drawer (--ci-z-drawer 1205, over a 1200 scrim), so the list rendered *underneath* the
   panel holding the input: you type, Google answers, and nothing appears. It has to out-rank the dialog layer too,
   because the same lookup can be opened from one.

   It also arrives completely unstyled — a white box with Google's own greys — so it is themed here to the same
   surface/ink/edge tokens as the app's own popovers, or it is a white slab in dark mode. Google owns the internal
   class names, hence the third-party selectors; keep this the only place they appear. */
.pac-container {
    z-index: 1500;
    margin-top: 2px;
    border: 1px solid var(--ci-line);
    border-radius: var(--ci-radius);
    background: var(--ci-surface);
    box-shadow: var(--ci-shadow-lg);
    font-family: inherit;
}

/* Google's own attribution strip. Their terms require it stays, so it is toned down, never hidden. */
.pac-container:after { background-color: var(--ci-surface); opacity: .75; }

.pac-item {
    padding: .3rem .6rem;
    border-top: 1px solid var(--ci-line-faint);
    font-size: .84rem;
    color: var(--ci-ink-support);
    cursor: pointer;
}
.pac-item:first-child { border-top: 0; }
.pac-item:hover, .pac-item-selected, .pac-item-selected:hover { background: var(--ci-surface-2); }
.pac-item-query { font-size: .86rem; color: var(--ci-ink-body); }
.pac-matched { font-weight: 700; color: var(--ci-ink-body); }
.pac-icon { display: none; }        /* Google's sprite is a light-mode PNG; the text alone reads in both themes. */

/* ── Merge placeholders in a message body (ci-placeholders.js) ────────────────────────────────────────────────
   Shared rather than page-local because the JS module is shared: any screen where an author writes a message
   containing {---Field---} / [[---Button---]] / %%---Table---%% placeholders wires the same module and gets the
   same three affordances.

   THE HIGHLIGHT LAYER. A textarea can't style its own content, so .ci-phbox stacks two boxes that must agree on
   every metric that affects where a glyph lands: the mirror underneath paints the placeholder backgrounds with
   its own text transparent, and the real textarea sits on top with a transparent background and visible text.
   They share their font, padding and border through .ci-phbox > * — if you give one of them a different
   padding or line-height, the highlights slide out from under the words and it looks like a rendering bug.
   The reason for going to this trouble rather than reaching for a rich-text editor: this box holds the email
   HTML exactly as it will be SENT. An editor would reformat it on the way in. */
.ci-phbox { position: relative; }
.ci-phbox > * {
    font: inherit;
    font-size: .875rem;
    line-height: 1.5;
    font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, Consolas, monospace;
    padding: .5rem .65rem;
    border: 1px solid var(--ci-line-strong);
    border-radius: var(--ci-radius-sm);
    box-sizing: border-box;
    white-space: pre-wrap;
    overflow-wrap: break-word;
}
.ci-phbox-mirror {
    position: absolute;
    inset: 0;
    overflow: hidden;
    color: transparent;
    border-color: transparent;
    background: var(--ci-surface);
    pointer-events: none;
}
/* GOTCHA, and it cost a probe to find: the selector has to be this specific. The theme's own rule for bare
   controls — `textarea:not([class*="e-input"]):not([class*="e-control"])` — carries TWO :not() attribute
   selectors, so it outranks a plain `.ci-phbox-input` and painted an opaque white background straight over the
   highlight layer. The placeholders were all correctly marked up in the mirror and none of them were visible. */
.ci-phbox > textarea.ci-phbox-input {
    position: relative;
    width: 100%;
    display: block;
    background: transparent;
    color: var(--ci-ink-body);
    resize: vertical;
    min-height: 16rem;
}
.ci-phbox-input:focus { outline: 2px solid var(--ci-accent); outline-offset: -1px; }

/* A placeholder, wherever it is shown: inside the body, and as a chip in the rail. Three families because they
   do three different things to the email, and a tint is a formula off the brand/status colour so both themes
   come out of one declaration. `bad` is the one that earns its keep — a placeholder the merge has never heard
   of arrives in the client's inbox as literal braces, and before this it did so silently. */
mark.ci-ph { background: none; color: inherit; border-radius: var(--ci-radius-xs); }
mark.ci-ph-val { background: color-mix(in srgb, var(--ci-accent) 16%, transparent); }
mark.ci-ph-btn { background: color-mix(in srgb, var(--ci-ai-ink) 18%, transparent); }
mark.ci-ph-tbl { background: color-mix(in srgb, var(--ci-ok) 18%, transparent); }
mark.ci-ph-bad {
    background: color-mix(in srgb, var(--ci-danger) 16%, transparent);
    box-shadow: inset 0 -2px 0 var(--ci-danger);
}

/* THE TYPE-AHEAD, opened by typing `{` in any placeholder field. Built by the JS module and appended to <body>,
   because it is anchored to the CARET rather than to an element — there is nothing in the render tree to hang it
   off. Same shape as every other anchored panel in the app: a floating surface, so it gets a real shadow and the
   standard 4px arrival (its selector is in the ci-pop-in list). */
.ci-ph-pop {
    position: fixed;
    z-index: var(--ci-z-anchored);
    min-width: 15rem;
    max-width: 22rem;
    max-height: 15rem;
    overflow: auto;
    padding: .25rem;
    background: var(--ci-surface);
    border: 1px solid var(--ci-line-strong);
    border-radius: var(--ci-radius);
    box-shadow: var(--ci-shadow-lg);
}
.ci-ph-opt {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .3rem .45rem;
    border-radius: var(--ci-radius-sm);
    font-size: .84rem;
    color: var(--ci-ink-body);
    cursor: pointer;
}
/* Keyboard selection and hover are the SAME state deliberately: the arrow keys and the mouse are choosing from
   one list, and two different highlights in one popup reads as two selections. */
.ci-ph-opt.is-on, .ci-ph-opt:hover { background: var(--ci-hover); }
.ci-ph-dot { width: .5rem; height: .5rem; border-radius: var(--ci-radius-pill); flex: 0 0 auto; }
.ci-ph-dot.ci-ph-val { background: var(--ci-accent); }
.ci-ph-dot.ci-ph-btn { background: var(--ci-ai-ink); }
.ci-ph-dot.ci-ph-tbl { background: var(--ci-ok); }
.ci-ph-lbl { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ci-ph-grp { color: var(--ci-ink-faint); font-size: .74rem; flex: 0 0 auto; }

/* ── Working feedback ───────────────────────────────────────────────────────────────────────────
   Painted while a click has gone unanswered — see wwwroot/ci-busy.js for when that is and why. This
   is the whole visible half of the fix for "I clicked it and nothing happened, so I clicked again".

   Two cues, deliberately, because they answer two different questions:
     the BAR    — "the application is working on it", readable from anywhere on a large screen;
     the CURSOR — "and it is this click it is working on", which is where the user is already looking.

   Both are drawn from the theme's own accent, so a workspace that has been recoloured gets its own
   colour here too, and both are inert to the pointer. Nothing is disabled, dimmed or covered: the page
   stays exactly as usable as it was, because the one thing worse than a slow response is a slow
   response that also locks the screen. */
html.ci-working, html.ci-working body { cursor: progress; }
/* …but never over text the user may be trying to select or a field they are typing in — a wait cursor
   on a text box reads as "this field is unavailable", which is the opposite of true. */
html.ci-working :is(input, textarea, select, [contenteditable="true"]) { cursor: auto; }

html.ci-working::after {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    z-index: var(--ci-z-working);
    pointer-events: none;
    background: linear-gradient(90deg,
        transparent 0%,
        var(--ci-accent) 35%,
        var(--ci-accent) 65%,
        transparent 100%);
    /* Indeterminate, and it has to be: there is no progress to report — the server has the click and we
       do not know what it is doing with it. A sweep says "still working" honestly; a bar that fills to
       90% and waits would be inventing a number. */
    background-size: 45% 100%;
    background-repeat: no-repeat;
    animation: ci-working-sweep 1.15s ease-in-out infinite;
}

@keyframes ci-working-sweep {
    from { background-position: -50% 0; }
    to   { background-position: 150% 0; }
}

/* Reduced motion gets the same information without the travel: a steady accent line across the top.
   The cue is load-bearing, so it is never removed outright — only the movement is. */
@media (prefers-reduced-motion: reduce) {
    html.ci-working::after {
        background: var(--ci-accent);
        opacity: .75;
        animation: none;
    }
}
