/* --- Typeface ----------------------------------------------------------------------------------
   Source Sans 3, self-hosted. A humanist sans chosen for long reading sessions after Manrope's
   geometric, tightly-spaced forms proved fatiguing over hours of use — open letterforms and
   unambiguous I/l/1 disambiguation trade off the closer rhyme with the wordmark for less eye
   strain. Served from this RCL rather than a font CDN: an external font host is a third-party
   request on every page load, the same reason no icon font is loaded from one.

   One variable file per subset covers the whole 400-700 range the app uses, so the 500/600/650
   weights in this file are real instances rather than synthetic bolding. latin-ext is not optional
   here — it is where the Hungarian ő and ű live. */
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url(../fonts/source-sans-3-latin.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url(../fonts/source-sans-3-latin-ext.woff2) format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Shared visual-language foundation: the role vocabulary every screen-level stylesheet reads its
   colour from, plus the app shell's ground — a tinted canvas with the workspace floating on it as
   a rounded sheet.

   The role names below are ours; their values are always resolved through the
   --dxds-* -> --dxbl-* -> light-mode-hex chain, never a ported palette hex, so the DX theme
   picker, Fluent Dark and every accent preset keep working. Consumers reference --vl-*; they do
   not restate the resolution. */
:root {
    /* The one place the app's typeface is stated. Every Bootstrap and DX font-family resolves
       through this single token — the Fluent theme defines --bs-body-font-family and each
       component's own font-family as var(--dxds-font-family-sans-serif) — so re-pointing it here
       reaches the whole UI, and no component needs a rule of its own. The theme defines it at
       :root too, in its core stylesheet, which loads before this file: the tie goes to load order.
       The system chain stays behind Source Sans 3 as the fallback if the woff2 ever fails to load. */
    --dxds-font-family-sans-serif: 'Source Sans 3', var(--dxds-font-family-segoe-ui), var(--dxds-font-family-system-sans-serif);

    /* The type scale. Five steps, and a surface picks one — the sizes were previously spelled out
       as literals at each use site, which is how the chrome and the forms drifted two steps apart.
       Named smallest first: the uppercase overline (grid and group captions), the status strip,
       the chrome's reading size (grid cells, toolbar), the form's reading size, and a panel title.
       Body/form text is one step ABOVE the chrome deliberately: a caption the user reads while
       typing carries more weight than a cell being scanned. */
    --vl-font-overline: 10.5px;
    --vl-font-xs: 11px;
    --vl-font-sm: 12.5px;
    --vl-font-md: 13px;
    --vl-font-lg: 14px;

    /* Canvas: the tinted ground the workspace floats on. */
    --vl-canvas: var(--dxds-color-surface-neutral-subdued-rest, var(--dxbl-page-bg-color, #f4f5f7));
    /* Surface: the sheet and anything reading as foreground on the canvas. */
    --vl-surface: var(--dxds-color-surface-neutral-default-rest, var(--dxbl-page-bg-color, #fff));
    /* Subdued fill sitting on a surface (band headers, quiet rows). */
    --vl-surface-2: var(--dxds-color-surface-neutral-subdued-rest, var(--dxbl-page-bg-color, #f0f0ee));

    /* Both rules are a percentage of the text colour over the surface rather than DX's border token.
       That token is a mid grey in BOTH modes (#d3d3d2 light, #767676 dark), so against a near-black
       dark surface it is a far bigger step than the same token against white — a cage in dark, a
       hairline in light. Stating the rule as a share of the text colour makes the step symmetric:
       the same number reads the same way whichever way the mode runs. */
    --vl-line: color-mix(in srgb, var(--vl-text) 14%, var(--vl-surface));
    /* Soft hairline: separators inside a surface, where the full line weight reads as a cage. */
    --vl-line-soft: color-mix(in srgb, var(--vl-text) 8%, var(--vl-surface));

    --vl-text: var(--dxds-color-content-neutral-default-rest, var(--dxbl-text-color, #333));
    --vl-text-muted: var(--dxds-color-content-neutral-subdued-rest, var(--dxbl-text-color-muted, #888));

    --vl-accent: var(--dxds-color-surface-primary-default-rest, var(--dxbl-accent-color-100, #1f6feb));
    --vl-accent-soft: color-mix(in srgb, var(--vl-accent) 12%, var(--vl-surface));
    --vl-accent-softer: color-mix(in srgb, var(--vl-accent) 7%, var(--vl-surface));
    /* The accent darkened towards the text colour: the accent itself is a fill colour and reads too
       light as a glyph or caption on the surface. */
    --vl-accent-text: color-mix(in srgb, var(--vl-accent) 82%, var(--vl-text));

    /* Destructive verb. A muted brick rather than a signal red: the glyph is a 1.5px stroke on the
       sheet, and a saturated red at that weight shouts louder than the accent it sits next to.
       Not themed — the mid tone clears both the white and the near-black sheet. */
    --vl-danger: #c0524d;

    /* Text and glyphs sitting ON the accent fill. Brand-fixed by design: it must keep its
       contrast against the accent in both modes, so it does not track the theme. */
    --vl-on-accent: #fff;

    /* Geometry, measured from the reference mockup. */
    --vl-sheet-radius: 12px;
    --vl-control-radius: 7px;
    /* Height of the chrome stacked above the workspace body — header + tab strip + view
       toolbar. Single source of truth for anything that has to hang below it (the Notes and
       Follow drawers anchor here). */
    --vl-chrome-top: 147px;
    /* Translucent shadows, deliberately not themed: they fade rather than invert in dark mode.
       Four lifts, smallest first — a card sitting on a surface, the workspace sheet, a drawer or
       flyout floating over everything, and a modal sheet floating over the scrim. */
    --vl-shadow-card: 0 1px 2px rgba(0, 0, 0, .08);
    --vl-sheet-shadow: -2px -1px 10px rgba(0, 0, 0, .05);
    --vl-shadow-panel: 0 .5rem 1rem rgba(0, 0, 0, .15);
    --vl-shadow-popup: 0 12px 40px rgba(16, 17, 20, .28), 0 2px 8px rgba(16, 17, 20, .16);
    /* The faint fill that marks "this field takes input". One recipe, because a control that is
       not a .dxbl-text-edit (the colour-palette trigger) has to paint it by hand.
       Mixed from the accent rather than the text colour: at these percentages a neutral grey is
       still grey, and a form is dozens of fields deep, so the greyness compounds into the whole
       page. The accent carries the theme's own hue at the same weight of mark, which is why it also
       tracks every accent preset instead of needing a second value per theme. */
    --vl-editor-fill: color-mix(in srgb, var(--vl-accent) 3%, transparent);
    /* Dark wash over a thumbnail, carrying --vl-on-accent glyphs. Brand-fixed for the same
       reason --vl-on-accent is: a theme-tracking wash would leave a white glyph unreadable in
       light mode. Hover deepens it. */
    --vl-media-scrim: rgba(0, 0, 0, .55);
    --vl-media-scrim-hover: rgba(0, 0, 0, .75);
    --vl-canvas-tint: linear-gradient(160deg, color-mix(in srgb, var(--vl-accent) 18%, var(--vl-canvas)), var(--vl-canvas) 70%);
}

/* Two roles are re-pointed for the dark themes, because DX's neutral ramp runs the other way there
   and "subdued" stops meaning what the roles above assume.
   Canvas: the subdued token lands one step below the sheet, too close to read as ground, so the
   ground takes the deep token instead. Surface-2: subdued would put a quiet row BELOW the sheet it
   sits on, a hole rather than a band, so it takes the step above.
   --vl-canvas-tint has to be restated: custom properties inherit as computed values, so the copy
   resolved at :root already carries the old canvas hex and no override reaches it. The fallbacks
   are the dark values — this block only applies once the dark theme class is on the body.
   --vl-surface is deliberately left alone: DX paints its own popups and dropdowns from that same
   token, and moving ours would seam them against the sheet. */
.dxbl-theme-dark {
    --vl-canvas: var(--dxds-color-surface-neutral-deep-rest, #161616);
    --vl-surface-2: var(--dxds-color-surface-neutral-default-selected, #2e2e2d);
    --vl-canvas-tint: linear-gradient(160deg, color-mix(in srgb, var(--vl-accent) 18%, var(--vl-canvas)), var(--vl-canvas) 70%);
    /* The editor fill needs twice the share here, and the reason is not symmetry: DX does NOT
       brighten the accent for dark mode — it stays the same mid blue in both — while the sheet
       drops to near-black. A percentage tuned to read against white therefore lands almost on top
       of the dark sheet: measured, the light share lifts the dark sheet by under half of what the
       grey fill it replaced did, so the mark all but disappears. Doubling it restores a matching
       perceived lift, and the blue channel carries most of that. */
    --vl-editor-fill: color-mix(in srgb, var(--vl-accent) 6%, transparent);
}

/* --- The ground -------------------------------------------------------------------------------
   XAF's Fluent theme paints the whole shell — header, sidebar and content — one flat surface, and
   gives the sidebar a mirror-image sheet treatment (top margin, top border, top-RIGHT radius).
   Here the canvas is the ground, header and navigation sit directly on it unpainted, and a single
   sheet is drawn on the content column. Selectors match XAF's own specificity and win on load
   order: this file is registered after DevExpress.ExpressApp.Blazor/styles.css. */

.dxbl-theme-fluent .app {
    background: var(--vl-canvas-tint);
}

.dxbl-theme-fluent .app .header {
    background: transparent;
    border-bottom: 0;
}

.dxbl-theme-fluent .app .sidebar {
    background: transparent;
    border: 0;
    margin-top: 0;
    border-top-right-radius: 0;
}

/* XAF sizes the navigation column for one accordion at 270px; the two-tier navigation needs the
   56px rail plus the 224px panel. Lives here rather than in the nav component's scoped stylesheet
   because it is the shell's column, not the component's own box. */
.app .xaf-sidebar.sidebar {
    width: 280px;
    min-width: 280px;
    /* Collapsing animates this column's width; the panel keeps its own 224px and is clipped away as
       the column narrows, so it slides out behind the rail instead of blinking off. */
    overflow: hidden;
    transition: width .2s ease, min-width .2s ease;
}

/* The workspace sheet. Scoped through .xaf-sidebar so it can only match the main window template —
   a popup's content lives under .dxbl-modal-body with no sidebar beside it, and a modal is already
   a lifted sheet that must not gain a second radius and shadow inside it. */
.dxbl-theme-fluent .app .xaf-sidebar.sidebar ~ .w-100 {
    background: var(--vl-surface);
    border-top: 1px solid var(--vl-line-soft);
    border-top-left-radius: var(--vl-sheet-radius);
    box-shadow: var(--vl-sheet-shadow);
}

/* Collapsing drops the tree panel and keeps the module rail — XAF's stock rule hides the whole
   column. The column keeps 10px more than the rail's 56px, and that strip is canvas, so the rail
   reads as detached from the sheet instead of butting against it. Only above the breakpoint: below
   it the same class is what opens the navigation as a drawer. */
@media (min-width: 576px) {
    .app.collapse-toggle .xaf-sidebar.sidebar {
        display: flex !important;
        width: 66px;
        min-width: 66px;
    }

    /* Clipping alone leaves the panel's rows in the tab order, so it is hidden outright — but only
       once it has finished sliding out. */
    .app.collapse-toggle .nav-panel {
        visibility: hidden;
        transition: visibility 0s .2s;
    }
}

/* Below XAF's sidebar breakpoint the navigation is hidden entirely, leaving the sheet's rounded
   corner stranded against nothing. */
@media (max-width: 575.98px) {
    /* The drawer's click-catching scrim is pinned at the stock 270px, so the widened column's last
       10px would close the drawer instead of selecting the row under the pointer. */
    .app.collapse-toggle .sidebar-sm-shadow {
        left: 280px;
    }

    .dxbl-theme-fluent .app .xaf-sidebar.sidebar ~ .w-100 {
        border-top: 0;
        border-top-left-radius: 0;
        box-shadow: none;
    }
}

/* --- Chrome: header strip, tab strip, view toolbars, status strip ------------------------------
   The four bands framing the workspace. Geometry and weight only — colour reads the roles above.

   Toolbar rules target the vl-* marker classes a universal action carries, never the rendered
   `data-action-name` attribute — that one holds the LOCALIZED caption.

   Every toolbar rule is prefixed .main-toolbar / .nested-toolbar / .dxbl-modal-footer: a bare
   .dxbl-btn-toolbar rule would also catch the header and collapse its right-aligned cluster. The
   popup footer is a toolbar with the same two-kind vocabulary, so it reads the same rules — the
   order scale puts Mégse at slot 10 and the filled primary at slot 60, with XAF's flex spacer
   between them at 55. */

.dxbl-theme-fluent .app .header {
    min-height: 48px;
    height: 48px;
}

.dxbl-theme-fluent .app .header .header-right-side {
    gap: 12px;
}

/* The hamburger cell continues the navigation rail's surface band up to the top of the window, so the
   screen's left edge is one unbroken column. Sized to the rail, and flush left — XAF's stock .5rem
   inset would leave a sliver of canvas beside it. */
.dxbl-theme-fluent .app .header .header-left-side {
    padding-left: 0;
}

.dxbl-theme-fluent .app .header .header-left-side .btn-hamburger {
    width: 56px;
    align-self: stretch;
    background: var(--vl-surface);
    border-radius: 0;
    color: var(--vl-text-muted);
    cursor: pointer;
}

/* The brand mark belongs to the header, not to the navigation column XAF hides along with it. */
.dxbl-theme-fluent .app:not(.main-ribbon-template).collapse-toggle .header-logo {
    display: block !important;
}

/* Same feedback as the rail rows below it — it is the top cell of the same band. */
.dxbl-theme-fluent .app .header .header-left-side .btn-hamburger:hover {
    background: var(--vl-surface);
    color: var(--vl-accent-text);
}

/* MDI tab strip. Scoped through .xaf-tabbed-mdi and the tablist child — never `.dxbl-tabs`
   alone, which under TabbedMDI wraps the entire view body. Colour and metric knobs go through
   DX's own --dxbl-tabs-* custom properties (allowed: this is a DX component's interior). */
.dxbl-theme-fluent .xaf-tabbed-mdi.dxbl-tabs > .dxbl-tabs-tablist {
    --dxbl-tabs-tab-font-size: var(--vl-font-md);
    --dxbl-tabs-tab-padding-x: 16px;
    /* Fluent draws the tab's underline as a pseudo-element filled with --dxbl-tabs-tab-*-bg — the var
       is the indicator's colour, not the tab's background. Transparent at rest and on the accent when
       active is what leaves the strip flat with one accent rule under the open tab; hover previews it
       in a soft tint. */
    --dxbl-tabs-tab-bg: transparent;
    --dxbl-tabs-tab-default-bg: transparent;
    --dxbl-tabs-tab-active-bg: var(--vl-accent);
    --dxbl-tabs-tab-hover-bg: color-mix(in srgb, var(--vl-accent) 30%, transparent);
    --dxbl-tabs-tab-active-hover-bg: var(--vl-accent);
    --dxbl-tabs-tab-color: var(--vl-text-muted);
    --dxbl-tabs-tab-default-color: var(--vl-text-muted);
    --dxbl-tabs-tab-hover-color: var(--vl-text);
    --dxbl-tabs-tab-active-color: var(--vl-text);
    --dxbl-tabs-tab-active-hover-color: var(--vl-text);
    --dxbl-tabs-tab-active-font-weight: 600;
    /* The indicator is positioned against the TAB, which ends 3px above the strip's own rule (the
       tablist's 1px border plus 2px of ul padding), so at DX's stock 0 it floats above the line
       instead of sitting on it. Pulling it down by that gap lands it exactly on the rule and lets
       it replace that segment, which is the mark the mockup draws. */
    --dxbl-tabs-tab-border-bottom: -3px;
    border-bottom: 1px solid var(--vl-line-soft);
}

/* View toolbars. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) {
    --dxbl-btn-toolbar-spacing: 2px;
    padding: 8px 16px;
}

/* Reordering XAF's mid-row flex spacer, rather than collapsing it, is what left-aligns the row:
   DX's adaptive measurement is built around the spacer, so it keeps its stock flex behaviour. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > .dxbl-toolbar-placeholder {
    order: 55;
}

/* Flat order scale. XAF gives every action its own .dxbl-btn-group, so those groups are the flex
   row's items and ordering them is enough. Unmarked (screen-specific) actions trail at slot 50;
   vl-ord-<n> lifts the universal verbs into fixed slots, and the ellipsis overflow group closes
   the row. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > * {
    order: 50;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-10) {
    order: 10;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-11) {
    order: 11;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-12) {
    order: 12;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-13) {
    order: 13;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-20) {
    order: 20;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-21) {
    order: 21;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-22) {
    order: 22;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-25) {
    order: 25;
}

/* The list-scoped search box ends the row (per the rule doc's §Grid). */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > :has(.vl-ord-60) {
    order: 60;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > .dxbl-toolbar-btn-ellipsis {
    order: 70;
}

/* Button weight: quiet text on the sheet, no DX secondary box. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar .dxbl-btn {
    padding: 4px 10px;
    border-radius: 5px;
    font-size: var(--vl-font-sm);
    font-weight: 500;
    color: var(--vl-text-muted);
    background: transparent;
    border-color: transparent;
    box-shadow: none;
}

/* Toolbar glyphs take a theme colour instead of their stock hues. They arrive as an `<img>` from
   XAF's image service, not as inline SVG, so neither `fill` nor `color` reaches the artwork; the
   handle is a mask. toolbar-glyph-tint.js copies each glyph's own URL into --vl-glyph, which turns
   the element into its own silhouette so `background-color` paints it — one colour per glyph, from
   the same tokens as everything else, tracking every accent preset and both modes.
   Two mechanics: the attribute guard keeps the rule off until the property exists (an undefined
   --vl-glyph would drop the mask and leave a solid coloured box), and `object-position` parks the
   original bitmap outside its own content box, which the mask alone would leave showing through.
   `currentColor` is the default paint, which gets the filled action's white and the disabled
   action's grey for free — the button already states both, and the glyph follows.
   The toolbar dropdown is in the scope because a split button's menu items and the overflow menu
   are the same actions, carrying the same markers, rendered in a popup portal; leaving them out
   left a stock multicolour glyph sitting directly under a tinted one. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer, .dxbl-toolbar-dropdown) :is(.dxbl-btn-toolbar, .dxbl-toolbar-dropdown-item-list) img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: currentColor;
    mask-image: var(--vl-glyph);
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: var(--vl-glyph);
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    object-fit: none;
    object-position: -999px -999px;
}

/* A dropdown's own entries take the accent too. Their markers live on the parent action, not on
   the entries, so `currentColor` left a menu of ink glyphs hanging under an accent button — the
   export format list being the visible case. The disabled entry is restored to `currentColor`
   right after, which is where DX's own dimming already is. */
.dxbl-toolbar-dropdown .dxbl-toolbar-dropdown-item-list img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: var(--vl-accent-text);
}

.dxbl-toolbar-dropdown .dxbl-toolbar-dropdown-item-list :is(:disabled, .dxbl-disabled) img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: currentColor;
}

/* Same mask idiom for the shell chrome — hamburger, home and the notifications bell — which
   render their glyph as an `<img>` the same way toolbar actions do (toolbar-glyph-tint.js tags
   all three). A Material Symbols SVG carries no `fill`, so without the mask
   the glyph would show as a plain black square regardless of theme. */
:is(.btn-hamburger, .home-nav-anchor, .user-notifications-anchor) img[style*="--vl-glyph"] {
    width: 24px;
    height: 24px;
    flex: 0 0 auto;
    background-color: currentColor;
    mask-image: var(--vl-glyph);
    mask-size: 24px;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: var(--vl-glyph);
    -webkit-mask-size: 24px;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    object-fit: none;
    object-position: -999px -999px;
}

/* Same mask idiom for the MDI tab strip's own class image (e.g. the Model Difference editor's tab
   icon) — DX renders it as a plain `<img>` too, and toolbar-glyph-tint.js's selector widens to
   include it. currentColor already tracks the tab strip's own --dxbl-tabs-tab-*-color custom
   properties (set above), so the glyph follows active/inactive/hover for free. */
.xaf-tabbed-mdi .dxbl-tabs-tablist img[style*="--vl-glyph"] {
    background-color: currentColor;
    mask-image: var(--vl-glyph);
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: var(--vl-glyph);
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    object-fit: none;
    object-position: -999px -999px;
}

/* Hover feedback. Our flat rule above outranks DX's own hover state, so both weights restate it: a
   quiet button gains a soft fill, the filled one deepens. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar .dxbl-btn:hover:not(:disabled) {
    background: color-mix(in srgb, var(--vl-text) 6%, transparent);
    color: var(--vl-text);
}

/* The save cluster is the one filled action in the row. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar :is(.dxbl-btn.vl-primary, .vl-primary > .dxbl-btn) {
    background: var(--vl-accent);
    color: var(--vl-on-accent);
    font-weight: 600;
    padding: 4px 12px;
    border-radius: 6px;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar :is(.dxbl-btn.vl-primary, .vl-primary > .dxbl-btn):hover:not(:disabled) {
    background: color-mix(in srgb, var(--vl-accent) 88%, var(--vl-text));
    color: var(--vl-on-accent);
}

/* The universal verbs take ONE glyph colour, because their members are peers: grading them by kind
   puts three hues in a six-button row, which is the busyness the whole toolbar treatment removes.
   That includes the save cluster's quiet members (slot 11) — universal verbs that happen to keep a
   caption, and leaving them ink put a grey glyph in the middle of a tinted row.
   --vl-accent-text rather than --vl-accent: the accent is a fill colour, tuned so white text sits on
   it, and reads washed as a 1.5px stroke on the sheet. Hover lifts to the full accent — a step in
   weight, not a second hue.
   Painted on the glyph rather than through the button's `color` so a visible caption stays ink: a
   captioned action goes accent in the glyph only. The disabled guard is stated here rather than
   left to the block below, because that block reaches the glyph through `currentColor` and this
   rule would otherwise outrank it. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer, .dxbl-toolbar-dropdown) :is(.dxbl-btn-toolbar, .dxbl-toolbar-dropdown-item-list) :is(.dxbl-btn.vl-uni, .vl-uni > .dxbl-btn, .dxbl-btn.vl-ord-11, .vl-ord-11 > .dxbl-btn):not(:disabled, .dxbl-disabled) img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: var(--vl-accent-text);
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer, .dxbl-toolbar-dropdown) :is(.dxbl-btn-toolbar, .dxbl-toolbar-dropdown-item-list) :is(.dxbl-btn.vl-uni, .vl-uni > .dxbl-btn, .dxbl-btn.vl-ord-11, .vl-ord-11 > .dxbl-btn):hover:not(:disabled, .dxbl-disabled) img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: var(--vl-accent);
}

/* Delete is the one verb the row cannot take back, so it leaves the shared accent and states that
   in colour. Stated after the accent rules at matching specificity, and it keeps their disabled
   guard so an unavailable delete still reads grey rather than red. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer, .dxbl-toolbar-dropdown) :is(.dxbl-btn-toolbar, .dxbl-toolbar-dropdown-item-list) :is(.dxbl-btn.vl-ord-13, .vl-ord-13 > .dxbl-btn):not(:disabled, .dxbl-disabled) img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: var(--vl-danger);
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer, .dxbl-toolbar-dropdown) :is(.dxbl-btn-toolbar, .dxbl-toolbar-dropdown-item-list) :is(.dxbl-btn.vl-ord-13, .vl-ord-13 > .dxbl-btn):hover:not(:disabled, .dxbl-disabled) img.xaf-toolbar-item-icon[style*="--vl-glyph"] {
    background-color: color-mix(in srgb, var(--vl-danger) 82%, var(--vl-text));
}

/* Unavailable actions. Every rule above states ONE colour for all states, which is what left a
   disabled action looking identical to a live one while silently refusing the pointer — DX's own
   disabled tint never gets a chance to land. So the state is restated here, last, where it outranks
   both weights on load order.
   Colour is the thing withdrawn: caption and glyph alike go neutral at a third of the ink — one
   declaration, since the masked glyph paints itself with the button's own colour. `:disabled` is the
   attribute DX writes on a plain button, `.dxbl-disabled` the class it puts on a wrapper, and an
   action can arrive either way. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar .dxbl-btn:is(:disabled, .dxbl-disabled) {
    color: color-mix(in srgb, var(--vl-text) 34%, transparent);
    cursor: default;
}

/* A filled accent button that does nothing is the loudest lie the toolbar can tell, so a disabled
   primary keeps its shape and gives up the fill.
   The leading .dxbl-theme-fluent is what makes this land, and it is not decoration: XAF ships a
   six-class rule for exactly this button
   (`.dxbl-theme-fluent:not(.dxbl-theme-dark) .xaf-primary-toolbar-btn.dxbl-btn.dxbl-btn-primary.dxbl-disabled`)
   which paints a pale-accent fill at 0.8 opacity. It writes `background` and `opacity` directly
   rather than through --dxbl-btn-disabled-*, so re-pointing those properties does nothing at all —
   the declarations have to be restated here, at matching specificity, winning on load order. The
   theme class also covers dark, where XAF's own rule excludes itself. */
.dxbl-theme-fluent :is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar :is(.dxbl-btn.vl-primary, .vl-primary > .dxbl-btn):is(:disabled, .dxbl-disabled) {
    background: color-mix(in srgb, var(--vl-text) 10%, transparent);
    color: color-mix(in srgb, var(--vl-text) 34%, transparent);
    opacity: 1;
}

/* Universal verbs collapse to a glyph square. min-width rather than width so a split button's
   caret is not clipped. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar :is(.dxbl-btn.vl-uni, .vl-uni > .dxbl-btn) {
    min-width: 26px;
    height: 26px;
    padding: 4px;
    justify-content: center;
    gap: 0;
}

/* One caret for the whole row. DX draws a dropdown two ways and sizes them differently: a button
   that owns its menu gets a 20px caret svg inline after the glyph, while a split button's second
   half gets a 12px one — side by side in the same toolbar that reads as a mistake. The svg is the
   handle (the chevron inside scales with it), so one size fixes both shapes.
   The 8px DX puts between glyph and caret is the other half of it: on a collapsed universal verb
   that gap is wider than the glyph's own padding, so a single button reads as two. Only the inline
   caret takes the margin — in a split button the caret is its half's only child and already
   centred. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar .dxbl-btn > .dxbl-toolbar-dropdown-toggle {
    width: 16px;
    height: 16px;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar .dxbl-btn > .dxbl-toolbar-dropdown-toggle:not(:only-child) {
    margin-left: 2px;
}

/* The split button's caret half, narrowed to what a 16px caret needs, so a split dropdown and an
   inline one come out the same total width — the two shapes stand next to each other in the row
   (create and export), and matching the caret alone still left a step in the button. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar .dxbl-btn-split > .dxbl-btn-split-dropdown {
    min-width: 22px;
    padding-left: 2px;
    padding-right: 2px;
}

/* DX's no-item-text mode hides every caption; the two-kind rule needs the opposite default.
   `!important` overrides DX's two dozen enumerated display:none paths in one rule. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-group:not(:has(.vl-uni)) .dxbl-btn > .dxbl-image + span {
    display: inline-block !important;
}

/* A screen-specific action stays text and drops its image, UNLESS it carries a deliberately
   assigned family glyph (vl-icon) — a consistent icon family is worth drawing even off the
   universal-verb cluster. Both DX guards stay load-bearing regardless: a dropdown toggle's caret
   is itself a .dxbl-image, and an image with no caption sibling is the whole button. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(:has(.vl-icon)) .dxbl-btn > .dxbl-image:not(:only-child):not(.dxbl-toolbar-dropdown-toggle) {
    display: none;
}

/* The universal verb's caption stays in the DOM, screen-reader-only: it is the accessible name
   once the glyph stands alone. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-group:has(.vl-uni) .dxbl-btn > .dxbl-image + span {
    display: inline-block !important;
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* Cluster fence: one hairline in the row, between the universal glyph cluster and the first
   screen-specific action. It needs no marker class — a screen-specific action is exactly an item
   carrying no vl-ord-<n>, and the second rule takes the fence back off every later one, so only the
   leader keeps it. The placeholder and the overflow ellipsis are excluded: neither is an action,
   and the placeholder would strand a hairline in the middle of the empty gap.
   Drawn on the group wrapper so the rule is 16px tall regardless of the button's own box. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(.dxbl-toolbar-btn-ellipsis) {
    position: relative;
    margin-left: 8px;
    padding-left: 9px;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(.dxbl-toolbar-btn-ellipsis)::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 1px;
    height: 16px;
    background: var(--vl-line);
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(.dxbl-toolbar-btn-ellipsis) ~ .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(.dxbl-toolbar-btn-ellipsis) {
    margin-left: 0;
    padding-left: 0;
}

:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .dxbl-btn-toolbar > .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(.dxbl-toolbar-btn-ellipsis) ~ .dxbl-btn-group:not(:has([class*="vl-ord-"])):not(.dxbl-toolbar-btn-ellipsis)::before {
    content: none;
}

/* --- Grid: rows, headers, states, filter affordance -------------------------------------------
   Unscoped .dxbl-grid on purpose: every list view migrates, nested lists and lookup popups
   included. Metric and colour knobs go through DX's own --dxbl-grid-* custom properties (allowed:
   a DX component's interior); this file loads after the Fluent theme, so equal-specificity rules
   win on order. Mechanism notes live in docs/ai/conventions/ui-visual-language.md §Grid. */

.dxbl-grid {
    --dxbl-grid-font-size: var(--vl-font-sm);
    --dxbl-grid-text-cell-padding-x: 16px;
    --dxbl-grid-border-color: var(--vl-line-soft);
    --dxbl-grid-header-bg: transparent;
    --dxbl-grid-header-color: var(--vl-text-muted);
    --dxbl-grid-header-font-weight: 600;
    --dxbl-grid-hover-bg: var(--vl-surface-2);
    --dxbl-grid-hover-color: var(--vl-text);
    --dxbl-grid-selection-bg: var(--vl-accent-softer);
    --dxbl-grid-selection-color: var(--vl-text);
    --dxbl-grid-selected-hover-bg: var(--vl-accent-soft);
    /* Focused AND selected must not become a third shade. */
    --dxbl-grid-selection-focus-dark-bg: var(--vl-accent-soft);
    --dxbl-grid-selection-focus-dark-color: var(--vl-text);
    --dxbl-grid-selection-focus-dark-hover-bg: var(--vl-accent-soft);
    --dxbl-grid-focus-bg: var(--vl-accent-softer);
    --dxbl-grid-focus-color: var(--vl-text);
    font-variant-numeric: tabular-nums;
    /* No box around the list: it sits on the workspace sheet, and the row rules are the only ruling. */
    border-width: 0;
    border-radius: 0;
}

/* Columns are not ruled. Fluent draws a border-left on every cell and nothing between rows; the
   row rule comes back on the cell height rule below, which already excludes the spacer rows. */
.dxbl-grid .dxbl-grid-table > :is(thead, tbody) > tr > :is(th, td) {
    border-left-width: 0;
}

/* Header's one mark is its bottom rule. Full path so the rule ties with DX's own th rule. */
.dxbl-grid .dxbl-grid-table > thead .dxbl-grid-header-row > th {
    padding: 6px 16px;
    font-size: var(--vl-font-overline);
    letter-spacing: .07em;
    text-transform: uppercase;
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--vl-line-soft);
}

/* A td treats height as a minimum, so image, edit and multi-line rows still grow. The virtual
   scroller's two spacer rows are exempt: they carry the off-screen extent as an inline height on
   their FIRST cell only, so a blanket row height inflates them into an empty band above the first
   row and below the last. */
.dxbl-grid .dxbl-grid-table > tbody > tr:not([dxbl-top-virtual-spacer-element]):not([dxbl-bottom-virtual-spacer-element]) > td {
    height: 34px;
    border-bottom: 1px solid var(--vl-line-soft);
}

/* Same mask idiom as the toolbar glyphs, for the row command column. The <img> itself carries no
   class at all here, so the scope runs through the action's own wrapper div (.xaf-inline-action) —
   toolbar-glyph-tint.js tags through the same wrapper. Every action image in the model resolves to
   vendored artwork, so tinting applies to all of them, marked or not; the grid's own status icons
   (including the semantic validation state, which must keep its colour) render in a different DOM
   shape and are never nested inside an .xaf-inline-action. currentColor here already tracks both
   the row's own ink and DX's native :disabled opacity (checked against the live computed style,
   not assumed), so no extra colour rule is needed. */
.dxbl-grid .dxbl-grid-command-cell .xaf-inline-action img[style*="--vl-glyph"] {
    background-color: currentColor;
    mask-image: var(--vl-glyph);
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: var(--vl-glyph);
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    object-fit: none;
    object-position: -999px -999px;
}

/* The row's Edit command is the grid component's own button, not an XAF action, so its glyph
   arrives as <use> references into the DevExpress sprite sheet and the image service never sees
   it — the mask idiom above finds no <img> to read a URL from. Hiding the sprite references and
   masking the <svg> itself with the image service's canonical Action_Edit URL puts the glyph back
   on the one family: the hash-free form of that URL resolves through the same embedded-resource
   alias every toolbar Edit already uses, so re-pointing that alias moves this glyph with it. The
   URL is root-absolute because the stylesheet is served three directories deep under _content. */
.dxbl-grid-command-cell .dxbl-grid-command-button > svg.dxbl-image:has(use[href*="dx-grid-edit-row"]) {
    background-color: currentColor;
    mask-image: url("/IImageService/Action_Edit");
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: url("/IImageService/Action_Edit");
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
}

.dxbl-grid-command-cell .dxbl-grid-command-button > svg.dxbl-image:has(use[href*="dx-grid-edit-row"]) use {
    display: none;
}

/* Filter funnel: no box of its own, and hidden at rest. Scoped to the header row so the grouping
   band's chips keep their permanently visible funnels. */
.dxbl-grid .dxbl-grid-header-row .dxbl-btn.dxbl-grid-filter-menu-funnel-btn {
    padding: 2px;
    opacity: 0;
    transition: opacity .12s ease;
}

.dxbl-grid .dxbl-grid-header-row:hover .dxbl-btn.dxbl-grid-filter-menu-funnel-btn,
.dxbl-grid .dxbl-grid-header-row .dxbl-btn.dxbl-grid-filter-menu-funnel-btn:is(:focus-visible, .dxbl-grid-filter-menu-funnel-btn-filled, .dxbl-grid-filter-menu-funnel-btn-active) {
    opacity: 1;
}

/* An active filter also tints the caption, so the column reads as filtered without hovering. */
.dxbl-grid .dxbl-grid-table > thead .dxbl-grid-header-row > th:has(.dxbl-grid-filter-menu-funnel-btn-filled) {
    color: var(--vl-accent);
}

/* Numeric range filter menu body: sized to fit the filter-menu dropdown, not a chart-scale
   layout. Native <input type="range"> is DX's own accent, via accent-color. */
.column-filter-range {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px 12px;
    min-width: 220px;
}

.column-filter-range-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.column-filter-range-label {
    flex: 0 0 auto;
    color: var(--vl-text-muted);
    font-size: var(--vl-font-sm);
}

.column-filter-range-slider {
    width: 100%;
    accent-color: var(--vl-accent);
}

.column-filter-range-empty {
    color: var(--vl-text-muted);
    font-size: var(--vl-font-sm);
}

.column-filter-range-clear {
    align-self: flex-start;
    background: none;
    border: none;
    padding: 0;
    color: var(--vl-accent);
    font-size: var(--vl-font-sm);
    cursor: pointer;
}

/* Date filter menu: a checked period list, the Között date-range editor, and the value list
   below a separator. The fixed root height is what keeps the popup from resizing as the value
   list appears and disappears; the period list and the value list each scroll inside it. */
.column-filter-date-menu {
    display: flex;
    flex: none;
    flex-direction: column;
    height: 340px;
    min-width: 220px;
}

.column-filter-date {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    flex-direction: column;
    gap: 6px;
    padding: 8px 12px;
}

.column-filter-date-periods {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    flex-direction: column;
    gap: 6px;
    overflow-y: auto;
}

/* Only while the value list is present do the periods give up room for it: exactly four rows
   (4 × 32px option + 3 × 6px gap), so the cut never lands mid-row. On their own — the value
   list is hidden whenever a period is selected — they take the whole menu instead of leaving
   dead space below. The menu's own height stays fixed either way, so the popup never resizes. */
.column-filter-date-menu:has(.column-filter-date-values) .column-filter-date {
    flex: none;
}

.column-filter-date-menu:has(.column-filter-date-values) .column-filter-date-periods {
    flex: none;
    height: 146px;
}

.column-filter-date-option {
    cursor: pointer;
}

/* min-height:0 lets this shrink below the checklist's own height; without it the checklist
   renders outside its box, over the periods above. */
.column-filter-date-values {
    display: flex;
    flex: 1 1 0;
    min-height: 0;
    overflow: auto;
    border-top: 1px solid var(--vl-line-soft);
}

.column-filter-date-between {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 4px;
    padding-top: 8px;
    border-top: 1px solid var(--vl-line-soft);
}

.column-filter-date-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.column-filter-date-label {
    flex: 0 0 auto;
    color: var(--vl-text-muted);
    font-size: var(--vl-font-sm);
}

.column-filter-date-warning {
    color: var(--vl-danger);
    font-size: var(--vl-font-sm);
}

.column-filter-date-clear {
    align-self: flex-start;
    background: none;
    border: none;
    padding: 0;
    color: var(--vl-accent);
    font-size: var(--vl-font-sm);
    cursor: pointer;
}

/* The list-scoped search box: a quiet inset field, not a bordered editor. */
:is(.main-toolbar, .nested-toolbar, .dxbl-modal-footer) .vl-ord-60 .dxbl-text-edit {
    --dxbl-text-edit-bg: color-mix(in srgb, var(--vl-text) 4%, transparent);
    --dxbl-text-edit-border-color: color-mix(in srgb, var(--vl-text) 9%, transparent);
    /* Fluent draws a hard dark underline at rest; the box is one soft outline, nothing more. */
    --dxbl-text-edit-underline-color: color-mix(in srgb, var(--vl-text) 9%, transparent);
    --dxbl-text-edit-border-radius: var(--vl-control-radius);
    --dxbl-text-edit-font-size: var(--vl-font-sm);
    width: 190px;
}

/* --- Detail views: layout groups, editors ------------------------------------------------------
   Unscoped inside .dxbl-fl on purpose: every detail view migrates, mockup or not. No LayoutGroup
   is tagged — at ~1400 groups across ~140 views the treatment has to be the default, with
   CustomCSSClassName left for the exceptions. Mechanism notes live in
   docs/ai/conventions/ui-visual-language.md §Detail views. */

/* The form's reading size, set once on the layout root. Untouched, a detail view runs at the Fluent
   theme's own body size, two steps above the chrome around it — which is what made the forms and
   the lists read as different applications.

   All three go through DX's own custom properties, and a plain `font-size` here would reach neither
   of the first two: DX declares an explicit font-size on `.dxbl-fl` itself AND another on
   `.dxbl-fl .dxbl-fl-cpt`, so an inherited size loses to the caption's own declaration and the
   labels silently keep the theme's size while everything around them shrinks.
   The editor property is set here rather than on the editor rule below so it covers every editor
   kind at once — display views, memos, the ones nested in a tab. It cannot leak into a nested lines
   grid: DX re-points that same property to --dxbl-grid-font-size on the grid element, which is
   deeper and so wins for an inline editor. */
.dxbl-fl {
    --dxbl-fl-font-size: var(--vl-font-md);
    --dxbl-fl-caption-font-size: var(--vl-font-md);
    --dxbl-text-edit-font-size: var(--vl-font-md);
}

/* The group keeps a hairline box, but no fill of its own — the caption and the fields' shared
   alignment still carry most of the mark. Written as --dxbl-group-* on the group element (tying
   with DX's own rule, winning on load order) rather than as a hard background, so
   .xaf-totals-band's explicit accent fill still outranks it. */
.dxbl-fl .dxbl-fl-group > .dxbl-group {
    --dxbl-group-border-width: 1px;
    --dxbl-group-border-color: var(--vl-line-soft);
    --dxbl-group-border-radius: var(--vl-control-radius);
    --dxbl-group-bg: transparent;
    --dxbl-group-header-bg: transparent;
}

/* Styled on the header, not on its text node: a collapsible group hosts .dxbl-group-expand-btn
   here, which must keep working — so the caption is quieted, never hidden. */
.dxbl-fl .dxbl-fl-group > .dxbl-group > .dxbl-group-header {
    padding: 12px 12px 8px;
    font-size: var(--vl-font-overline);
    font-weight: 650;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--vl-text-muted);
}

/* Content on the caption's own x, so the label column and the caption share a left edge. */
.dxbl-fl .dxbl-fl-group > .dxbl-group > .dxbl-group-body > .dxbl-group-body-content {
    padding: 0 12px 10px;
}

/* Editors: a bottom rule and a faint fill, not four sides. Only the border WIDTHS are ours —
   the colour stays DX's --dxbl-text-edit-border-color, so hover, invalid and valid-modified keep
   tinting the remaining rule. .dxbl-text-edit is the shared root of the text, combo, lookup, date,
   spin, tag and memo editors; .dxbl-text-display-view is XAF's read-only rendering and is not a
   field being edited. A checkbox is not a .dxbl-text-edit at all, so it never picks this up.

   Load-bearing: --dxbl-text-edit-border-radius stays SINGLE-VALUED. DX sizes the focus bar with
   max(--underline-focused-height, --border-radius), and a four-value radius makes that max()
   invalid CSS, silently killing the focus rule — hence the corner shape as a direct declaration.
   Focus itself needs no rule of ours: Fluent already swaps the underline to the accent colour. */
.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view) {
    --dxbl-text-edit-border-radius: 0;
    --dxbl-text-edit-underline-focused-height: 1.5px;
    border-width: 0 0 1px;
    border-radius: 5px 5px 0 0;
}

/* The fill marks "this field takes input", so a disabled or read-only editor must not carry it —
   DX paints those transparent and leaves the muted text as the state's only mark. Our var sits at
   (0,4,0) and would otherwise outrank DX's (0,2,0) state rules. `.dxbl-disabled` on the editor
   root is the state class for both, read off the live DOM; the `is-read-only` attribute rides
   along with it and needs no selector of its own. */
.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view):not(.dxbl-disabled) {
    --dxbl-text-edit-bg: var(--vl-editor-fill);
}

/* Required-and-empty, read off XAF's `aria-required` and DX's own `field-text` mirror — see
   ui-visual-language-css.md. `:not(.invalid)` is load-bearing: both properties tie DX's own
   validation-red rule at (0,2,0), so without the guard the accent would outrank a failed save. */
.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view):not(.dxbl-disabled):not(.invalid):is([field-text=""], :not([field-text])):has([aria-required="true"]) {
    --dxbl-text-edit-border-color: var(--vl-accent);
    --dxbl-text-edit-underline-color: var(--vl-accent);
}

/* Width restated here because DX's own `invalid` rule supplies only the colour. */
.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view):not(.dxbl-disabled):is(.invalid, :is([field-text=""], :not([field-text])):has([aria-required="true"])) {
    border-bottom-width: 3px;
}

/* Trailing editor buttons (lookup pencil, dropdown chevron, spin arrows): the icon alone, no
   chrome. DX fills the button group with its own opaque background and puts a side border on each
   button, which reads as a separate control welded to the field; the group also rounds itself from
   --dxbl-text-edit-border-radius, zeroed above, so it squared off the editor's rounded corner.
   Hover is the only state that gets a surface, and it sits on the button so it stays a round chip.
   Scoped to direct children so a tag's own close button keeps DX's treatment; the editor-level
   clear X is excluded too — it is already flat, and DX renders it as a circle by design. */
.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view) > :is(.dxbl-btn-group, .dxbl-btn-group-vertical) {
    background-color: transparent;
}

.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view) > .dxbl-btn:not(.dxbl-edit-btn-clear),
.dxbl-fl .dxbl-fl-ctrl > .dxbl-text-edit:not(.dxbl-text-display-view) > :is(.dxbl-btn-group, .dxbl-btn-group-vertical) > .dxbl-btn {
    --dxbl-btn-bg: transparent;
    --dxbl-btn-hover-bg: color-mix(in srgb, var(--vl-text) 8%, transparent);
    --dxbl-btn-active-bg: color-mix(in srgb, var(--vl-text) 12%, transparent);
    --dxbl-btn-disabled-bg: transparent;
    --dxbl-btn-border-radius: 5px;
    border-width: 0;
}

/* A lookup's New and Edit buttons hold their glyph on a plain <div class="xaf-image dxbl-image">
   that XAF masks from an inline `mask:` shorthand carrying no size, so the 48px artwork lands
   uncropped in a 20px box and only its centre shows — the pencil arrives as two bare diagonal
   strokes and the plus as an oversized cross beside the dropdown chevron. Every other .xaf-image
   XAF renders (nav chrome, avatar, empty-data) gets an explicit mask-size; only these two do not.
   `!important` is load-bearing: the inline shorthand resets mask-size to `auto` on the element
   itself, which a plain stylesheet declaration cannot outrank. Unscoped by editor context — the
   same button renders inside a form, a grid's inline-edit row and a popup. */
.dxbl-text-edit .dxbl-btn > .xaf-image.dxbl-image {
    mask-size: contain !important;
    -webkit-mask-size: contain !important;
}

/* --- Popups: scrim, sheet, label-above ---------------------------------------------------------
   The footer's button weight and ordering come from the toolbar section above; only the shell and
   the label placement are here. */

/* No scrim rule of ours: DX already paints one. `.dxbl-modal-back` is a fixed, full-viewport
   sibling of `.dxbl-modal-root` at z-index -1 and it renders rgba(0,0,0,.4) for every DX modal,
   XAF popups included — a second wash on the root only stacked with it and doubled the dim. */

/* An editor's own dropdown list continues the field it hangs off: square where the two meet, round
   at the free bottom edge. DX rounds all four corners from --dxbl-edit-dropdown-border-radius, and
   the list inside carries its own corners off the dropdown vars, so the clip is what keeps the
   panel and its content to one shape. Both selectors are the same panel — DX renders it inline or
   as a modal popup depending on the editor. */
:is(.dxbl-dropdown, .dxbl-itemlist-dropdown) > .dxbl-dropdown-dialog.dxbl-edit-dropdown,
.dxbl-modal > .dxbl-modal-root > .dxbl-popup.dxbl-edit-dropdown > .dxbl-modal-content {
    border-radius: 0 0 5px 5px;
    overflow: hidden;
}

.dxbl-modal > .dxbl-modal-root > .dxbl-popup.xafPopup,
.dxbl-modal > .dxbl-modal-root > .dxbl-popup.xafPopup > .dxbl-modal-content {
    border-radius: var(--vl-sheet-radius);
}

.dxbl-modal > .dxbl-modal-root > .dxbl-popup.xafPopup {
    box-shadow: var(--vl-shadow-popup);
}

.dxbl-modal .dxbl-modal-content > .dxbl-modal-header {
    font-size: var(--vl-font-lg);
    font-weight: 650;
}

/* Label above editor: a modal is narrow, and the inline form's label column would squeeze the
   editors. DX lays items out horizontally from its own min-width block — a media query adds no
   specificity, so these ties win on load order.

   The .vl-line-form exemption on all four: a document line opens in a tab-sized popup, not a narrow
   dialog, and that form has to render exactly as it does in a tab — same label-left flow, same
   .xaf-cap-w-* caption bands. The marker sits on the line view's Main layout group
   (CustomCSSClassName), so it lands inside the modal rather than on its shell — hence :has(). */
.dxbl-modal:not(:has(.vl-line-form)) .dxbl-fl .dxbl-fl-item.dxbl-fl-item-horizontal {
    flex-flow: column nowrap;
}

.dxbl-modal:not(:has(.vl-line-form)) .dxbl-fl .dxbl-fl-item.dxbl-fl-item-horizontal > .dxbl-fl-cpt {
    flex: 0 0 auto;
    width: auto;
}

/* The .xaf-cap-w-* band is meaningless once the label is above — it would pad every caption out
   to the group's widest one. */
.dxbl-modal:not(:has(.vl-line-form)) .dxbl-fl .dxbl-fl-cpt {
    min-width: 0;
}

/* DX re-shows the placeholder caption for a horizontal item; the class survives the flow override,
   so without this every caption-less editor in a popup gains a blank label row. */
.dxbl-modal:not(:has(.vl-line-form)) .dxbl-fl .dxbl-fl-item.dxbl-fl-item-horizontal > .dxbl-fl-empty-caption {
    display: none;
}

/* …and the line form's own half of the exemption, which has to be stated positively. Inside a modal
   DX never puts the .dxbl-fl-item-horizontal class on an item at all — the form layout measures its
   container while the modal is still unsized, so every item keeps the stacked default and the four
   rules above have nothing to spare. Restoring label-left therefore means re-asserting DX's own
   horizontal rules on plain .dxbl-fl-item, at the same 992px viewport gate it uses. */
@media (min-width: 992px) {
    .dxbl-modal:has(.vl-line-form) .dxbl-fl .dxbl-fl-item {
        flex-flow: row nowrap;
    }

    .dxbl-modal:has(.vl-line-form) .dxbl-fl .dxbl-fl-item > .dxbl-fl-cpt {
        flex: 1 0 auto;
        width: var(--dxbl-fl-caption-width-large-screen);
    }

    .dxbl-modal:has(.vl-line-form) .dxbl-fl .dxbl-fl-item > .dxbl-fl-empty-caption {
        display: flex;
    }
}

/* Status strip pinned to the bottom of the workspace sheet. */
.vl-workspace-body {
    flex: 1;
    min-height: 0;
}

.vl-status {
    display: flex;
    align-items: center;
    gap: 14px;
    flex: none;
    padding: 6px 16px;
    border-top: 1px solid var(--vl-line-soft);
    font-size: var(--vl-font-xs);
    line-height: 1.2;
    color: var(--vl-text-muted);
}

/* Field-level validation-rule surface (admin-mode toggle icon + its anchored popup). The glyph
   arrives through DxEditorButton's IconCssClass, which — unlike an XAF action image — has no
   image-service <img> for the tint script to copy a URL from, so the mask URL is written
   statically here: the image service answers the same vendored artwork without the fingerprint
   query string every action image also carries (same idiom as the grid's row Edit command, which
   masks a component-rendered <svg> the same way). Quiet by default (muted glyph); a field that
   already carries a rule tints to the accent so an administrator can see at a glance which fields
   are governed — one mark: the tint alone states that fact, nothing else changes. */
.uvr-field-icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    color: var(--vl-text-muted);
    background-color: currentColor;
    mask-image: url("/IImageService/Action_Validation_Validate");
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: url("/IImageService/Action_Validation_Validate");
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
}

.uvr-field-icon--has-rules {
    color: var(--vl-accent);
}

/* The item quick-search box's in-field settings button. */
.item-search-gear {
    display: inline-block;
    width: 16px;
    height: 16px;
    color: var(--vl-text-muted);
    background-color: currentColor;
    mask-image: url("/IImageService/Action_Settings");
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: url("/IImageService/Action_Settings");
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
}

.uvr-field-menu {
    background: var(--vl-surface);
    color: var(--vl-text);
    border: 1px solid var(--vl-line);
    border-radius: var(--vl-sheet-radius);
    box-shadow: var(--vl-shadow-popup);
    width: 280px;
    max-width: calc(100vw - 24px);
}

.uvr-field-menu-header {
    padding: 8px 12px;
    border-bottom: 1px solid var(--vl-line-soft);
    font-weight: 600;
}

.uvr-field-menu-items,
.uvr-field-menu-rules {
    display: flex;
    flex-direction: column;
}

.uvr-field-menu-rules {
    border-top: 1px solid var(--vl-line-soft);
    max-height: 220px;
    overflow-y: auto;
}

.uvr-field-menu-item,
.uvr-field-menu-rule {
    width: 100%;
    text-align: left;
    justify-content: flex-start;
}

.uvr-field-menu-rule-inner {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

.uvr-field-menu-rule-meta,
.uvr-field-menu-rule-readonly {
    font-size: var(--vl-font-xs);
    color: var(--vl-text-muted);
}

.uvr-field-menu-empty {
    padding: 12px;
    color: var(--vl-text-muted);
    font-style: italic;
}

/* Linked breakdown panel: a native vertical resize handle instead of a splitter component. */
.linked-breakdown-panel {
    resize: vertical;
    overflow: auto;
    min-height: 120px;
}

/* Split overview: the head list and the line pane share one column of definite height, divided by
   the head pane's native vertical resize grip — the same route as the panel above, with no
   splitter component. Both marker classes are put on by split-overview.js, because neither DX
   element they land on is reachable from the layout model. The column has to be built the whole
   way down from the view's content box: every wrapper between is DX's own and sizes to content,
   so a percentage height anywhere below would resolve against `auto` and collapse. */
.split-overview-host,
.split-overview-host > .dxbl-fl,
.split-overview-host > .dxbl-fl > .dxbl-row,
.split-overview > .dxbl-row {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    min-height: 0;
}

.split-overview-host {
    height: 100%;
}

.split-overview-host > .dxbl-fl,
.split-overview-host > .dxbl-fl > .dxbl-row,
.split-overview {
    flex: 1 1 0;
    min-height: 0;
}

/* The group itself stays the block box DX renders it as — its `display` is not ours to win — so
   the flex column is its inner row, sized by a percentage against the definite height the group
   has already taken from the flex chain above it. */
.split-overview > .dxbl-row {
    height: 100%;
}

/* The head pane is the one that carries the grip, so it is the one with the explicit height; the
   line pane takes what is left. Both floors, plus the head pane's ceiling, are what stops a drag
   from collapsing either list — the ceiling leaves room for the header band and the line pane's
   own floor. */
.split-overview .split-heads {
    flex: 0 0 auto;
    height: var(--split-heads, 50%);
    min-height: 160px;
    max-height: calc(100% - 200px);
    resize: vertical;
    overflow: auto;
}

.split-overview .split-divider {
    flex: 0 0 auto;
}

.split-overview .split-lines {
    flex: 1 1 0;
    min-height: 160px;
    overflow: auto;
}

/* Each grid fills its own pane and scrolls inside it, instead of standing at content height and
   letting the page scroll — a shared height has no other way to be shared. The layout item's own
   control box takes the pane's height outright; below it the height has to be *shared* rather than
   repeated, because the hosted list view puts its toolbar above the grid — `height: 100%` there
   would push the grid's bottom past the pane edge by the toolbar's height. */
.split-overview .split-heads > .dxbl-fl-ctrl,
.split-overview .split-lines > .dxbl-fl-ctrl,
.split-overview .nested-frame {
    height: 100%;
    min-height: 0;
}

.split-overview .nested-frame,
.split-overview .nested-content {
    display: flex;
    flex-direction: column;
}

.split-overview .nested-content,
.split-overview .grid-content {
    flex: 1 1 0;
    min-height: 0;
}

/* DX's grid is a flex column whose scroll viewer already grows to fill it, but the grid itself has
   no height of its own and stands at content height inside the box above. Both declarations are
   needed: XAF sizes the grid only under parents this screen does not have, and separately caps a
   nested virtual-scrolling one at 300px. */
.split-overview .split-heads .grid-content .dxbl-grid,
.split-overview .split-lines .grid-content .dxbl-grid {
    height: 100%;
    max-height: 100%;
}
