/* The DevExpress-free primitives (card grid, card item, badges, avatars) keep their rules in
   DevOp.Blazor.Shared.Primitives so a host without DevExpress or Bootstrap can load them alone. Every host
   that links this file gets them through this import, so none has to add a second <link>. The path is
   relative to this file (_content/DevOp.Blazor.Shared/), fingerprinted or not. An @import must precede
   every rule, so it stays first. */
@import url("../DevOp.Blazor.Shared.Primitives/primitives.css");

/* ---------------------------------------------------------------- focus rings

   On navigation the shell moves focus to the page's <h1> (tabindex="-1") so a screen reader announces
   where it just landed. The browser then draws its default focus ring around the heading, and every page
   in the app appears to have a black box round its title.

   Suppressing it only for :not(:focus-visible) is not enough: on a hard load or a refresh there is no prior
   user interaction to judge by, so the browser treats the scripted focus as keyboard-driven and draws the
   ring anyway — which is precisely when people saw the box.

   tabindex="-1" means the element is NOT in the tab order, so no keyboard user ever lands on it by tabbing.
   A focus ring there communicates nothing to anyone; the announcement is the point, not the outline. This
   only ever applies to elements a script has focused deliberately, never to a real control. */
[tabindex="-1"]:focus {
    outline: none;
}

.app-right-drawer {
    --app-right-drawer-width: min(100vw, 700px);
    /* The drawer supplies its own header and body chrome, so DevExpress must not pad the panel. */
    --dxbl-drawer-panel-body-padding-x: 0;
    --dxbl-drawer-panel-body-padding-y: 0;
    min-height: 100dvh;
}

.app-right-drawer .dxbl-drawer-panel {
    width: var(--app-right-drawer-width);
    max-width: 100vw;
}

/* The shell page can be taller than the viewport (the footer sits below the fold), and the panel's
   default height:100% would size it to the page — cutting its bottom off below the fold and leaving
   it looking half-extended. Pin the floating panel to the viewport instead. Overlap mode only: a
   Shrink-mode panel is in normal flow beside the content and must keep its in-flow height. */
.app-right-drawer.dxbl-drawer-overlap > .dxbl-drawer-panel {
    position: fixed;
    top: 0;
    bottom: 0;
    height: auto;
}

.app-right-drawer-open .dxbl-drawer-panel {
    box-shadow: -0.75rem 0 2rem rgba(15, 23, 42, 0.18);
}

.app-right-drawer .dxbl-drawer-panel > .dxbl-drawer-header {
    width: 100%;
    padding: 0;
}

.app-right-drawer .dxbl-drawer-panel > .dxbl-drawer-body {
    width: 100%;
    padding: 0;
}

.app-right-drawer-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    width: 100%;
    padding: 1rem 1.25rem;
    border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}

.app-right-drawer-title {
    min-width: 0;
}

.app-right-drawer-title h3 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    font-size: 1rem;
    line-height: 1.35;
}

.app-right-drawer-title h3 span,
.app-right-drawer-subtitle {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-right-drawer-subtitle {
    display: block;
    margin-top: 0.25rem;
    color: var(--bs-secondary-color);
    font-size: 0.8125rem;
}

.app-right-drawer-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 0 0 auto;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.app-right-drawer-body {
    width: 100%;
    padding: 1rem;
    overflow-x: hidden;
}

.app-right-drawer-frame {
    display: flex;
    flex-direction: column;
    width: 100%;
    min-height: 100%;
}

.app-right-drawer-frame > .app-right-drawer-body {
    flex: 1 1 auto;
}

.app-right-drawer-footer {
    position: sticky;
    bottom: 0;
    z-index: 1;
    width: auto;
    padding: 0.875rem 1rem 1rem;
    border-top: var(--bs-border-width) solid var(--bs-border-color);
    background: var(--bs-body-bg);
    box-shadow: 0 -0.5rem 1rem rgba(15, 23, 42, 0.04);
}

.app-right-drawer-body.app-right-drawer-body-flush {
    padding: 0;
}

/* ---------------------------------------------------------------- modal (DevExpress bridge) */

/* AppModal renders a DxPopup. The bootstrap-external theme already draws the Bootstrap modal: .dxbl-modal
   sets --dxbl-popup-margin to Bootstrap's 1.75rem dialog margin, a top-aligned popup sits at that margin,
   and the footer is right-aligned. The bridge pins the margin (so a host on another DevExpress theme keeps
   the Bootstrap placement) and adds the one thing the theme lacks, the .modal-footer gap between buttons.
   CssClass may land on the dialog rather than the modal root, so the selectors are descendant ones. */
.app-modal {
    --dxbl-popup-margin: 1.75rem;
}

.app-modal .dxbl-modal-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
}

/* ---------------------------------------------------------------- offcanvas (DevExpress bridge) */

/* AppOffcanvas is a DxPopup pushed against a viewport edge. A sheet has no rounded corners and no gutter,
   and the theme's 500px popup cap must not hold back a full-width top or bottom sheet. */
.app-offcanvas {
    --dxbl-popup-border-radius: 0;
    --dxbl-popup-margin: 0;
    --dxbl-popup-max-width: 100vw;
}

/* ---------------------------------------------------------------- progress (DevExpress bridge) */

/* AppProgress renders a DxProgressBar, whose indicator colour is a DevExpress CSS variable rather than a
   Bootstrap .bg-* class. The colour token lands on the root as app-progress-<token>; these rules resolve it
   to the theme's --bs-<token> (the extended palette carries Bootstrap's literal as a fallback, because a
   theme need not define --bs-purple and friends) and hand it to DevExpress. Soft is the badge-soft look:
   a 25% tint of the colour for the bar, the full colour for the label. */
.app-progress {
    width: 100%;
    --dxbl-progress-bar-indicator-color: var(--app-progress-color);
}

.app-progress-primary { --app-progress-color: var(--bs-primary); --app-progress-color-rgb: var(--bs-primary-rgb); }
.app-progress-secondary { --app-progress-color: var(--bs-secondary); --app-progress-color-rgb: var(--bs-secondary-rgb); }
.app-progress-success { --app-progress-color: var(--bs-success); --app-progress-color-rgb: var(--bs-success-rgb); }
.app-progress-warning { --app-progress-color: var(--bs-warning); --app-progress-color-rgb: var(--bs-warning-rgb); }
.app-progress-info { --app-progress-color: var(--bs-info); --app-progress-color-rgb: var(--bs-info-rgb); }
.app-progress-danger { --app-progress-color: var(--bs-danger); --app-progress-color-rgb: var(--bs-danger-rgb); }
.app-progress-light { --app-progress-color: var(--bs-light); --app-progress-color-rgb: var(--bs-light-rgb); }
.app-progress-dark { --app-progress-color: var(--bs-dark); --app-progress-color-rgb: var(--bs-dark-rgb); }
.app-progress-purple { --app-progress-color: var(--bs-purple, #6f42c1); --app-progress-color-rgb: var(--bs-purple-rgb, 111, 66, 193); }
.app-progress-pink { --app-progress-color: var(--bs-pink, #d63384); --app-progress-color-rgb: var(--bs-pink-rgb, 214, 51, 132); }
.app-progress-blue { --app-progress-color: var(--bs-blue, #0d6efd); --app-progress-color-rgb: var(--bs-blue-rgb, 13, 110, 253); }
.app-progress-orange { --app-progress-color: var(--bs-orange, #fd7e14); --app-progress-color-rgb: var(--bs-orange-rgb, 253, 126, 20); }

/* The rgba form is the fallback: a custom property is not validated when it is parsed, so a browser without
   color-mix() would take the later declaration and paint nothing. The @supports gate is what makes the
   fallback real. */
.app-progress.app-progress-soft {
    color: var(--app-progress-color);
    --dxbl-progress-bar-indicator-color: rgba(var(--app-progress-color-rgb), 0.25);
}

@supports (color: color-mix(in srgb, red 25%, transparent)) {
    .app-progress.app-progress-soft {
        --dxbl-progress-bar-indicator-color: color-mix(in srgb, var(--app-progress-color) 25%, transparent);
    }
}

.entity-note-markdown-editor .dxbl-tabs-content-panel {
    padding: 0;
}

.entity-note-markdown-editor .note-preview {
    min-height: 10rem;
    padding: 0.75rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-body-bg);
}

.app-entity-option {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
}

.app-entity-option__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.app-entity-option__title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-entity-option__description {
    color: var(--bs-secondary-color);
    font-size: 0.8125rem;
}

.app-dropdown-icon-only,
.app-dropdown-icon-only .dxbl-btn-dropdown-toggle {
    width: 2rem;
    min-width: 2rem;
}

.app-dropdown-icon-only .dxbl-btn-caption {
    display: none;
}

/* A destructive item in an AppDropdown, e.g. Delete. Tinted so it reads as the dangerous one
   without being loud until hovered. */
.app-dropdown-item-danger {
    color: var(--bs-danger);
}

.app-dropdown-item-danger:hover {
    background-color: var(--bs-danger);
    color: #fff;
}

.app-state-info {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: 8rem;
    padding: 1.5rem;
    color: var(--bs-secondary-color);
    text-align: center;
}

.app-toolbar,
.app-toolbar-start,
.app-toolbar-content,
.app-toolbar-end {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.app-toolbar {
    justify-content: space-between;
}

.app-toolbar-content {
    min-width: 0;
    flex: 1 1 auto;
}

.app-grid-toolbar {
    padding: 0.75rem;
    border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}

.app-grid-search {
    min-width: min(24rem, 100%);
}

.app-grid-ai-filter-chip {
    display: inline-flex;
    align-items: center;
    max-width: 22rem;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    font-size: 0.8125rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--bs-primary-text-emphasis, var(--bs-primary));
    background-color: var(--bs-primary-bg-subtle, rgba(13, 110, 253, 0.1));
}

/* Ribbon command surface (AppGrid RibbonEnable). The DxRibbon panel renders at
   content width and the grid's toolbar container is padded (--dxbl-grid-toolbar-
   container-padding-x: 1rem), so the ribbon stops short and its items sit inset
   from the columns (cell padding is only .375rem). Stretch it to full width
   (higher specificity beats DX theme rules) and zero the toolbar container's
   horizontal padding so the ribbon is flush and its items align with the columns. */
.app-grid:has(.app-grid-ribbon) {
    --dxbl-grid-toolbar-container-padding-x: 0;
}

.app-grid .app-grid-ribbon {
    width: 100%;
    background-color: transparent;
    border: 0;
    /* Drop the ribbon's own bottom divider; the grid header already separates the
       command surface from the data, so one line reads calmer than stacked borders. */
    --dxbl-ribbon-border-bottom-width: 0;
}

/* Roomier grid rows + semibold header text for clearer column hierarchy. Vertical
   column lines are removed natively via the DxGrid GridlinesVisible="Gridlines.Horizontal"
   parameter (see AppGrid), not CSS. */
.app-grid {
    --dxbl-grid-text-cell-padding-y: 0.5rem;
    --dxbl-grid-header-font-weight: 600;

    /* Inter's digits are proportional by default: at 32px "1111111111" measures 130px against
       200px for "0000000000". Down a money column that reads as ragged decimals. Tabular figures
       equalise every digit's advance. Scoped to the grid on purpose — tabular digits look loose
       in prose, so this must not leak onto body text. */
    font-variant-numeric: tabular-nums;
}

/* Identifiers render in the self-hosted monospace, not in the body face. These are the strings
   people transcribe by hand, read aloud over the phone and paste into a support ticket — SKUs,
   kennitala, invoice/order/asset/project numbers, EAN/GLN, VAT numbers — and in Inter the pairs
   0/O and 1/l are close enough to cost a wrong record. A monospace separates 1 from l by
   construction, and JetBrains Mono carries its disambiguating zero in the DEFAULT outline, so this
   needs no font-feature-settings (Google's webfont builds strip those features — that is the whole
   reason Inter cannot do this itself).

   It costs nothing visually: the face was chosen because its x-height and cap-height match Inter's
   exactly, so an identifier set inline beside body text reads at the same size. It is ~6.6% wider
   per character, so any slot that can receive a long value should ellipsize.

   Prose is deliberately excluded — a note or a description is not an identifier — as are money,
   quantities and dates, which are already tabular and which nobody transcribes.

   A copy column joins the group unconditionally: it exists for machine-readable values, so its
   value is an identifier whether or not the caller thought to say so.

   Requires DevOp.Blazor.Velex >= 0.3.41, which self-hosts the face and points --bs-font-monospace
   at it. On an older Velex that token is a system-only stack, so these surfaces fall back to
   whatever monospace the OS happens to have — still legible, but the per-machine inconsistency
   this exists to end. See theme-src/VENDOR.md and NEXER-327. */
.app-identifier,
.app-grid-copy-column-value {
    font-family: var(--bs-font-monospace);
    font-variant-numeric: tabular-nums;
}

.app-grid-copy-column {
    display: flex;
    align-items: center;
    /* Tight on purpose. The button belongs to the value beside it, and at 0.5rem plus the button's
       own padding the two read as separate cell contents rather than one affordance. */
    gap: 0.25rem;
    min-width: 0;
}

.app-grid-copy-column-value {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The copy button is a quiet affordance, not a control competing with the value.
   
   Two things are corrected here, both through DevExpress's own custom properties rather than by
   out-specifying its selectors — the properties are the supported seam and survive a version bump.
   
   1. Its text-button hover and active colours resolve to #d8dfe7 on #323a46 — light-theme values.
      On a dark grid row that paints a near-white chip under the cursor. Deriving them from
      currentColor instead keeps the wash subtle and correct in either theme. (The same leak affects
      every soft/text secondary button in dark mode; this fixes it only where the column owns it.)
   2. Its 0.25rem padding sat on top of the flex gap, so the glyph floated 12px from the value it
      copies. Halved, for ~6px total.
   
   Resting opacity keeps a column of these from reading as a column of icons; hover and keyboard
   focus bring it fully forward. focus-visible, not focus, so a mouse click does not leave one lit. */
.app-grid-copy-column-copy {
    flex: 0 0 auto;

    /* Pulls the button 2px back toward the value on top of the wrapper's gap. The gap itself stays
       at 0.25rem because it also separates an AfterValueTemplate badge from the value, and a badge
       needs the breathing room the button does not — the button belongs to the value, a badge sits
       beside it. Together with the button's own padding this lands the glyph ~4px off the text. */
    margin-inline-start: -0.125rem;

    opacity: 0.55;
    transition: opacity 120ms ease-in-out;
}

/* Overrides `.dxbl-btn.dxbl-btn-text-secondary` in DevExpress's bootstrap-external theme, which
   sets these same properties at specificity 0-2-0 — so the single-class rule above cannot reach
   them, and its own "Apply Styles to Components" topic says as much. Three real classes here, not
   a repeated one: the copy button, as a DevExpress button, inside a copy column. That clears 0-2-0
   outright rather than tying it and depending on stylesheet order. */
.app-grid-copy-column .app-grid-copy-column-copy.dxbl-btn {
    /* No padding overrides here on purpose. The resting --dxbl-btn-padding-x/y come from
       `.dxbl-btn-standalone.dxbl-btn-text-secondary.dxbl-sm.dxbl-btn-icon-only` at 0-4-0, above
       this rule, while the hover/active/focus ones are not set that deep. Overriding only the set
       that can be reached makes the button change size under the cursor. Uniform DevExpress
       padding, with the gap and margin above doing the spacing, is the stable answer. */
    --dxbl-btn-hover-color: currentcolor;
    --dxbl-btn-active-color: currentcolor;
    --dxbl-btn-hover-border-color: transparent;
    --dxbl-btn-active-border-color: transparent;

    /* Plain rgba first as the fallback a browser without color-mix keeps. */
    --dxbl-btn-hover-bg: rgba(128, 128, 128, 0.18);
    --dxbl-btn-hover-bg: color-mix(in srgb, currentcolor 14%, transparent);
    --dxbl-btn-active-bg: rgba(128, 128, 128, 0.28);
    --dxbl-btn-active-bg: color-mix(in srgb, currentcolor 22%, transparent);
}

.app-grid-copy-column:hover .app-grid-copy-column-copy,
.app-grid-copy-column-copy:focus-visible {
    opacity: 1;
}

/* AppClipboard with a Url: the address and its copy button are one unit, so they sit close and wrap
   together rather than the button stranding on its own line. */
.app-clipboard-link {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    max-width: 100%;
}

.app-clipboard-link > a {
    overflow-wrap: anywhere;
}

/* Search hosted inside a ribbon item template (Home tab) */
.app-grid-ribbon-search-item {
    padding: 0;
}

.app-grid-ribbon-search-item:hover {
    background-color: transparent;
}

.app-grid-ribbon-search {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}

.app-grid-ribbon-search-box {
    min-width: min(16rem, 60vw);
}

/* Destructive ribbon command (Delete in the canonical actions group). */
.app-ribbon-item-danger {
    color: var(--bs-danger, #dc3545);
}

/* ---- Alternate views (cards / a page-supplied view) ----
   In an alternate view the DxGrid stays mounted with its FULL column set — it remains the single
   engine for search, filtering, sorting and paging, and both ExportToCsvAsync/ExportToXlsxAsync and
   the grid's own search read the *visible* columns. So only the data table is hidden; the column set
   is never swapped. The view layer renders in the grid's toolbar slot, which is why the toolbar
   container's padding is zeroed and its own row layout is reset here.

   Hidden so that it can still be measured, never with display:none (NEXER-1357). DevExpress sizes a
   column with a set Width by reading its <col> once the grid has a width, and raises any reading under
   the column's MinWidth (50 px by default) to that minimum. A display:none table reads 0, so every
   Width="11rem" column was written back as width: 50px — and because only the table was hidden, the
   grid itself never changed size when the table came back, so nothing measured it again. Since 0.15.0
   every grid's first render is the skeleton, so that hit every grid with a fixed column width. The
   scroll viewer that holds the table is collapsed to no height and made invisible instead: the table
   keeps its layout width, so its columns read their real size, and nothing in it is shown, focusable
   or in the accessibility tree. contain: size stops the still-laid-out table (44rem wide below 768 px)
   from widening a grid or flex parent through its min-content, which display:none never did: measured at
   390 px in a grid parent, the page scrolled to 962 px without it. Direct child only, so a scroll viewer
   inside a card is left alone. */
.app-grid.app-grid-alt-view > .dxbl-scroll-viewer {
    contain: size;
    flex: 0 0 0 !important;
    height: 0 !important;
    min-height: 0 !important;
    overflow: hidden !important;
    visibility: hidden !important;
}

/* The grid is a flex column, and in the table the scroll viewer is what grows to keep the pager at the
   bottom of a grid the page gives a height. Collapsed above, it no longer grows, so the bottom panel
   takes the free space instead. Measured on a 480 px grid with two short cards: without this the pager
   sat 131 px from the grid's top with 319 px blank below it; with it, 441 px, as with the old
   display:none rule and in the table. */
.app-grid.app-grid-alt-view > .dxbl-grid-bottom-panel {
    margin-top: auto;
}

.app-grid.app-grid-alt-view {
    --dxbl-grid-toolbar-container-padding-x: 0;
}

/* The grid's toolbar container is a flex ROW, so the view layer rendered into that slot became a flex
   item beside the ribbon instead of a block beneath it. Two symptoms, one cause: the card stacked into
   the ribbon, and the card grid — squeezed into a flex item of unconstrained width — laid its columns out
   in a single row running off-screen, so only the first card was visible.

   Selected via :has() on whatever element directly contains the layer, rather than by DevExpress class
   name, so this does not depend on guessing an internal class (the file already uses :has() for
   .app-grid:has(.app-grid-ribbon)). Belt-and-braces flex-wrap in case a descendant keeps the flex
   context. */
.app-grid.app-grid-alt-view *:has(> .app-grid-view-layer) {
    display: block;
    width: 100%;
    flex-wrap: wrap;
}


.app-grid-view-layer {
    display: block;
    /* flex-basis covers the case where a theme keeps the toolbar slot as a flex context. */
    flex: 1 0 100%;
    width: 100%;
    max-width: 100%;
    padding: 0.75rem 0;
}

.app-grid-card-view {
    width: 100%;
}

.app-grid-card {
    display: flex;
    height: 100%;
    flex-direction: column;
    border-radius: var(--bs-border-radius, 0.375rem);
    /* Also applied to a consumer's own card component below: a designed CardTemplate usually wraps its
       content in an AppCard, and that inner .card carries its own min-content — so the wrapper shrinking
       is not enough on its own. Customers stayed two-per-row for exactly that reason while the default
       card already reflowed to three. */
    /* A grid track cannot shrink below its item's min-content unless this is set, and the card's regions
       are nowrap — so an unbroken email or a long phone/currency row set the card's min-content wide and
       the track ignored the declared --acvg-min. The visible symptom was two very wide cards per row on a
       screen with room for three or four. The regions already clip their overflow, so shrinking is safe. */
    min-width: 0;
}

/* The ring is drawn on the wrapper, so anything between the wrapper's edge and the card's shows through
   it as a band of bare page. Two things put it there: the wrapper stretches to the row's height so
   neighbouring cards end level while the card sizes to its content, and a card written for normal page
   flow carries a stacking margin (mb-3 is the common one) that has no meaning inside a grid track. The
   card fills the wrapper and drops the margin, so the ring outlines the card and nothing else.

   !important is the only thing that beats that margin: it comes from Bootstrap's mb-3 spacing utility and
   every utility in that family is declared !important by design, so ordinary specificity loses however
   the rule is written. The card is also a flex item in a column wrapper, so the margin shortens it as
   well as displacing it — 199px of wrapper gave a 179px card — which is why height alone left the gap. */
.app-grid-card > .card {
    height: 100%;
    margin-bottom: 0 !important;
}

/* AppGrid's card view keeps its empty tracks (auto-fill), unlike a standalone AppCardViewGrid (auto-fit,
   see primitives.css). With auto-fit, three records at 1600px became three 525px columns while each card
   was capped at 480px, so a gap opened beside every card; with 300 records the same cards were 391px.
   auto-fill sizes the columns from --acvg-min whatever the record count — 1600px gives four 391px columns
   for three records and for three hundred — and the cards fill them, so there is nothing to cap
   (NEXER-1265 C1). The track keeps the min(…, 100%) floor and the 1fr max for the reasons primitives.css
   gives. Two classes, so this wins over the primitive whatever order the stylesheets load in; the skeletons
   use the same tracks so the placeholders are the width the cards will be. */
.app-card-view-grid.app-grid-card-view,
.app-card-view-grid.app-grid-card-skeletons {
    grid-template-columns: repeat(auto-fill, minmax(min(var(--acvg-min, 240px), 100%), 1fr));
}

/* An optional cap a page can still set through AppGrid.CardMaxWidth; the default is none. It sits on the
   card, not on the track max, so the column count still comes from --acvg-min. */
.app-card-view-grid > .app-grid-card {
    max-width: var(--acvg-max, none);
}

/* The card view's empty state (AppGrid's own, or the page's EmptyDataAreaTemplate) spans the whole row. */
.app-grid-card-view-empty {
    grid-column: 1 / -1;
}

.app-grid-card--selectable {
    cursor: pointer;
    outline: 2px solid transparent;
    outline-offset: 2px;
    transition: outline-color 0.12s ease-in-out, background-color 0.12s ease-in-out;
}

.app-grid-card--selectable:hover {
    outline-color: var(--bs-border-color, rgba(0, 0, 0, 0.175));
}

/* The open control of a card and of a compact list row (NEXER-1293): a native button, the container's first
   child, that takes keyboard focus and carries the accessible name, so the container itself no longer has
   to be a role="button" around the template's own controls. It is visually hidden by clipping, never by
   display: none or pointer-events: none: VoiceOver presses it through AXPress, and pointer-events: none
   makes WebKit tap its centre instead, landing on whatever lies beneath. It changes no layout or stacking;
   taps still go to the container's own click handler, and its focus ring is drawn on the container. */
.app-grid-card-open,
.app-grid-mobile-row-open {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.app-grid-card:has(> .app-grid-card-open:focus-visible) {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: 2px;
}

/* A ring alone left a selected card looking outlined rather than chosen — the same weight the theme uses
   for validation errors. The faint surface tint carries the state alongside it; the ring stays because a
   tint alone is not enough contrast to be the sole indicator. color-mix is skipped by browsers that lack
   it, which degrades to the ring on its own rather than to something unreadable. No shadow: the ring is
   drawn on the wrapper and a shadow on the same box reads as a second, blurred edge outside it. */
.app-grid-card--selected,
.app-grid-card--selected:hover {
    outline-color: var(--bs-primary, #0d6efd);
}

.app-grid-card--selected > .card {
    background-color: color-mix(in srgb, var(--bs-primary, #0d6efd) 4%, var(--bs-card-bg, #fff));
}

/* Card regions — opt-in from a CardTemplate so a wall of cards reads as a grid.
   Cards are already equal height (the CSS grid stretches them and .app-grid-card is height:100%);
   what varies is the interior. Meta chips and tag lists wrap differently per record, so one card's
   name sits level with another's phone number and the whole set reads as a collage.
   Each region reserves its line, so the same region lines up across neighbouring cards. */
.app-grid-card-header,
.app-grid-card-contact,
.app-grid-card-meta,
.app-grid-card-tags {
    min-height: var(--agc-line, 1.5rem);
}

/* Deliberately nowrap. Reserving a line only aligns rows if the content cannot claim a second one —
   a record with six chips would otherwise push its neighbours' regions out of alignment, which is the
   defect this exists to fix. Overflow is clipped rather than wrapped; the grid view remains the place
   to read every value, and the column set is unchanged either way. */
.app-grid-card-meta,
.app-grid-card-tags {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0.25rem;
    overflow: hidden;
    min-width: 0;
}

.app-grid-card-header,
.app-grid-card-contact {
    min-width: 0;
}

/* Pins the last region to the bottom of the card, so cards of differing content still end alike. */
.app-grid-card-tags {
    margin-top: auto;
}

/* Column picker (AppColumnPicker) */
.app-grid-ribbon-columns-item {
    padding: 0;
}

.app-column-picker-body {
    width: 100%;
    overflow-x: hidden;
}

.app-column-picker-search {
    margin-bottom: 0.5rem;
}

.app-column-picker-list {
    max-height: 24rem;
    overflow-x: hidden;
    overflow-y: auto;
}

.app-column-picker-row {
    padding: 0.2rem 0.15rem;
}

.app-column-picker-row:hover {
    background: var(--bs-tertiary-bg);
    border-radius: 0.25rem;
}

.app-column-picker-all {
    border-bottom: var(--bs-border-width) solid var(--bs-border-color);
    margin-bottom: 0.35rem;
    padding-bottom: 0.5rem;
}

.app-quick-link {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    height: 100%;
}

.app-quick-link-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
}

@media (max-width: 575.98px) {
    .app-right-drawer-header {
        padding: 0.875rem 1rem;
    }

    .app-right-drawer-body {
        padding: 0.875rem;
    }

    .app-right-drawer-footer {
        padding: 0.75rem 0.875rem 0.875rem;
    }

    .app-toolbar,
    .app-toolbar-start,
    .app-toolbar-content,
    .app-toolbar-end {
        flex-wrap: wrap;
    }
}

/* Below the tablet breakpoint the DxGrid compresses every column to fit the
   viewport, which clips all headers to "…". Give the table a minimum width so
   columns keep a readable size and the grid scrolls horizontally instead
   (NEXER-190). Pure CSS on purpose, so prerender and circuit start — before the
   breakpoint has reported — look as they always did. Only a grid that has
   actually switched to list rows (NEXER-1250, .app-grid-compact-rows) drops it;
   grids that opt into @bind-IsXSmall drop secondary columns and never had it. */
@media (max-width: 767.98px) {
    .app-grid:not(.app-grid-adaptive):not(.app-grid-compact-rows) .dxbl-grid-table {
        min-width: 44rem;
    }
}

/* ---- Compact list rows (AppGridMobileRows, below 768 px) ----
   The rows render in the view layer like cards, and the table is hidden by
   .app-grid-alt-view. On a page with no rows the table stays, so the grid's own
   loading / empty / error area shows; its header row means nothing there. */
.app-grid.app-grid-compact-rows .dxbl-grid-table > thead {
    display: none;
}

.app-grid-compact-rows .app-grid-view-layer {
    padding: 0;
}

.app-grid-mobile-rows {
    display: flex;
    flex-direction: column;
    width: 100%;
    min-width: 0;
}

/* Positioned (no z-index, so no stacking context) only to contain the visually hidden open button: its
   containing block is the row, so focusing it scrolls the row into view inside a scroll container. */
.app-grid-mobile-row {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-height: 44px;
    min-width: 0;
    padding: 0.75rem 1rem;
    border-bottom: var(--bs-border-width) solid var(--bs-border-color);
    background-color: var(--bs-body-bg);
    transition: background-color 0.12s ease-in-out;
}

.app-grid-mobile-row:last-child {
    border-bottom: 0;
}

/* As the table greys and strikes a row deleted elsewhere (.app-grid-row--deleted). A card greys everything
   but its checkbox, which stays usable as on a row. */
.app-grid-mobile-row--deleted .app-grid-mobile-row-body,
.app-grid-card--deleted > :not(.app-grid-card-select) {
    opacity: 0.55;
    text-decoration: line-through;
}

.app-grid-mobile-row--selected {
    background-color: var(--bs-primary-bg-subtle);
}

/* The tap target is the whole body, not a link inside it. */
.app-grid-mobile-row-body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    justify-content: center;
    gap: 0.125rem;
    min-width: 0;
    min-height: 44px;
}

.app-grid-mobile-row--interactive .app-grid-mobile-row-body {
    cursor: pointer;
}

.app-grid-mobile-row-body:has(> .app-grid-mobile-row-open:focus-visible) {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
    border-radius: var(--bs-border-radius-sm);
}

.app-grid-mobile-row-select {
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    margin: -0.5rem 0 -0.5rem -0.5rem;
}

.app-grid-mobile-row-line {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    min-width: 0;
}

.app-grid-mobile-row-title,
.app-grid-mobile-row-subtitle {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-grid-mobile-row-title {
    font-weight: 600;
    color: var(--bs-emphasis-color);
}

.app-grid-mobile-row-line--secondary {
    font-size: 0.875em;
    color: var(--bs-secondary-color);
}

.app-grid-mobile-row-badges,
.app-grid-mobile-row-meta {
    display: inline-flex;
    flex: 0 1 auto;
    align-items: center;
    gap: 0.25rem;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
}

.app-grid-mobile-row-meta-item + .app-grid-mobile-row-meta-item::before {
    content: "\00B7";
    margin-right: 0.25rem;
}

/* A page's CardTemplate is the row body; its card chrome would be a box inside a row. */
.app-grid-mobile-row--template .card {
    margin-bottom: 0 !important;
    border: 0;
    box-shadow: none;
    background: transparent;
}

.app-grid-mobile-row--template .card-body {
    padding: 0;
}

/* Floating column-chooser button shown on phone viewports (AppGrid
   MobileColumnChooserFab) so users can reveal columns hidden by the adaptive
   layout. Rendered over a real table only, never over cards or compact rows.
   Anchored to the viewport bottom-right, above content. */
.app-grid-fab {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 1030;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    border: none;
    border-radius: 50%;
    color: #fff;
    background-color: var(--bs-primary);
    box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.3);
    font-size: 1.1rem;
}

.app-grid-fab:hover,
.app-grid-fab:focus-visible {
    filter: brightness(1.08);
    outline: none;
}

/* ============================================================
   APP FILE VIEWER
   ============================================================ */

.app-file-viewer {
    width: 100%;
    height: 100%;
}

.app-file-viewer-pdf {
    display: flex;
    flex-direction: column;
    height: 600px;
    min-height: 600px;
}

.app-file-viewer-pdf-inner {
    flex: 1;
}

.app-file-viewer-image {
    align-items: center;
    display: flex;
    justify-content: center;
    padding: 1rem;
}

.app-file-viewer-img {
    border-radius: .375rem;
    box-shadow: 0 2px 8px rgba(0,0,0,.12);
    max-height: 80vh;
    max-width: 100%;
    object-fit: contain;
}

.app-file-viewer-fallback {
    align-items: center;
    color: var(--bs-secondary-color);
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 200px;
    padding: 2rem;
    text-align: center;
}

/* ============================================================
   APP RIGHT DRAWER ACCOUNT MENU
   ============================================================ */
.app-right-menu-content {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    min-height: 0;
    color: var(--bs-body-color);
}

.app-right-menu-scroll {
    flex: 1 1 auto;
    min-height: 0;
    padding: 1.15rem;
    overflow-y: auto;
}

.app-right-menu-profile {
    display: flex;
    align-items: center;
    gap: 0.95rem;
    min-width: 0;
    padding-bottom: 1.1rem;
}

.app-right-menu-profile-avatar {
    flex: 0 0 auto;
}

.app-right-menu-profile-text {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.app-right-menu-user-name,
.app-right-menu-user-email {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-right-menu-user-name {
    font-size: 1rem;
    font-weight: 600;
}

.app-right-menu-user-email {
    color: var(--bs-secondary-color);
    font-size: 0.85rem;
}

.app-right-menu-tenant {
    display: grid;
    gap: 0.55rem;
    margin: 0;
    padding: 0.95rem 0;
    border-top: var(--bs-border-width) solid var(--bs-border-color);
    border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}

.app-right-menu-tenant div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    min-width: 0;
}

.app-right-menu-tenant dt {
    flex: 0 0 auto;
    color: var(--bs-secondary-color);
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
}

.app-right-menu-tenant dd {
    min-width: 0;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.app-right-menu-section {
    display: grid;
    gap: 0.75rem;
    padding: 1.05rem 0;
    border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}

.app-right-menu-section-title {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    color: var(--bs-secondary-color);
    font-size: 0.85rem;
    font-weight: 600;
}

.app-right-menu-section-title i {
    width: 1.25rem;
    color: var(--bs-primary);
    text-align: center;
}

.app-right-menu-theme-options {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
}

.app-right-menu-theme-option {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    min-width: 0;
    min-height: 2.5rem;
    padding: 0.5rem 0.75rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    color: var(--bs-body-color);
    background: var(--bs-secondary-bg);
    font-weight: 600;
    text-decoration: none;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.app-right-menu-theme-option:hover:not(:disabled) {
    border-color: rgba(var(--bs-primary-rgb), 0.45);
    color: var(--bs-primary);
}

.app-right-menu-theme-option.is-active {
    border-color: rgba(var(--bs-primary-rgb), 0.55);
    color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.1);
}

.app-right-menu-theme-option:disabled {
    cursor: default;
    opacity: 0.7;
}

.app-right-menu-culture .culture-selector {
    color: var(--bs-body-color);
}

.app-right-menu-culture .culture-selector > span {
    display: none;
}

.app-right-menu-culture .culture-selector select {
    min-height: 2.5rem;
}

.app-right-menu-footer {
    flex: 0 0 auto;
    padding: 0.85rem 1.15rem;
    border-top: var(--bs-border-width) solid var(--bs-border-color);
    background: var(--bs-body-bg);
}

.app-right-menu-signout {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    width: 100%;
    min-height: 2.5rem;
    border: var(--bs-border-width) solid var(--bs-danger-border-subtle);
    border-radius: var(--bs-border-radius);
    color: var(--bs-danger);
    font-weight: 600;
}

.app-right-menu-signout:hover {
    color: var(--bs-danger-text-emphasis);
    background: var(--bs-danger-bg-subtle);
    text-decoration: none;
}

/* ---------------------------------------------------------------- form fields */

.app-field + .app-field {
    margin-top: 0.75rem;
}

.app-field-required {
    margin-left: 0.15rem;
    color: var(--bs-danger);
}

/* The App* editors wrap DevExpress data editors, whose chrome is driven entirely by --dxbl-text-edit-*
   custom properties. Bridging those to Bootstrap's form tokens is what makes an AppTextBox look like the
   .form-control next to it without restyling any DevExpress DOM. Scoped to .app-field so a bare DevExpress
   editor elsewhere on the page keeps its own look; every token has a fallback so the editors still look
   right when a host ships no theme at all. A theme-coupled stylesheet (the shell's) may override these. */
.app-field .dxbl-text-edit {
    --dxbl-text-edit-border-radius: var(--bs-border-radius, 0.375rem);
    --dxbl-text-edit-border-color: var(--bs-border-color, #dee2e6);
    --dxbl-text-edit-bg: var(--bs-body-bg, #fff);
    --dxbl-text-edit-color: var(--bs-body-color, #212529);
    --dxbl-text-edit-font-size: var(--bs-body-font-size, 1rem);
    --dxbl-text-edit-line-height: var(--bs-body-line-height, 1.5);
    --dxbl-text-edit-padding-x: 0.75rem;
    --dxbl-text-edit-padding-y: 0.375rem;
    --dxbl-text-edit-focus-border-color: color-mix(in srgb, var(--bs-primary, #0d6efd) 50%, white);
    --dxbl-text-edit-focus-shadow-color: var(--bs-focus-ring-color, rgba(13, 110, 253, 0.25));
    --dxbl-text-edit-focus-shadow-spread: var(--bs-focus-ring-width, 0.25rem);
    --dxbl-text-edit-validation-invalid-color: var(--bs-form-invalid-border-color, var(--bs-danger, #dc3545));
}

/* Bootstrap's .form-control-sm / .form-control-lg, on the classes DevExpress adds for SizeMode. */
.app-field .dxbl-text-edit.dxbl-sm {
    --dxbl-text-edit-border-radius: var(--bs-border-radius-sm, 0.25rem);
    --dxbl-text-edit-font-size: 0.875rem;
    --dxbl-text-edit-padding-x: 0.5rem;
    --dxbl-text-edit-padding-y: 0.25rem;
}

.app-field .dxbl-text-edit.dxbl-lg {
    --dxbl-text-edit-border-radius: var(--bs-border-radius-lg, 0.5rem);
    --dxbl-text-edit-font-size: 1.25rem;
    --dxbl-text-edit-padding-x: 1rem;
    --dxbl-text-edit-padding-y: 0.5rem;
}

/* ---------------------------------------------------------------- calendar */

/* AppCalendar wraps DxCalendar; the grid, header and selection are DevExpress's own DOM and are not restyled.
   These rules only dress the day number we render in DayCellTemplate: a marker dot under the number in a
   contextual tone (holiday, booked, today…) and a muted digit for the weekdays a work pattern never uses. */
.app-calendar-day {
    position: relative;
    display: inline-block;
    min-width: 1.6rem;
    line-height: 1.6rem;
}

.app-calendar-day-muted {
    opacity: 0.5;
}

.app-calendar-day-marked::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -0.1rem;
    width: 0.3rem;
    height: 0.3rem;
    border-radius: 50%;
    transform: translateX(-50%);
    background: currentColor;
}

.app-calendar-day-primary::after { background: var(--bs-primary); }
.app-calendar-day-secondary::after { background: var(--bs-secondary); }
.app-calendar-day-success::after { background: var(--bs-success); }
.app-calendar-day-info::after { background: var(--bs-info); }
.app-calendar-day-warning::after { background: var(--bs-warning); }
.app-calendar-day-danger::after { background: var(--bs-danger); }

/* ---------------------------------------------------------------- tag box */

/* AppTagBox wraps DxTagBox, so its chips, drop-down and remove buttons are DevExpress's own DOM — we do NOT
   restyle them. This class only lets the editor fill the field width. The earlier rules here set
   `display:flex; flex-direction:column` on the editor root and hand-rolled `.app-tagbox-tag*` chips; both were
   left over from the pre-DxTagBox control, and the column layout was actively wrong — it forced DevExpress's
   inline chip row to stack full-width, one pill per line. */
.app-tagbox {
    width: 100%;
}

/* Only used when the app supplies a TagTemplate: lay its content out beside the remove button we add. Default
   (untemplated) chips are DevExpress's own and are not touched. */
.app-tagbox-tag {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.app-tagbox-tag-remove {
    padding: 0;
    opacity: 0.6;
}

.app-tagbox-tag-remove:hover {
    opacity: 1;
}

/* ---------------------------------------------------------------- breadcrumbs */

/* The theme sets the divider to an icon-font codepoint ("\ea50"). An app that ships the theme's CSS but not
   that font gets a tofu box between every crumb — which is what the mail portal showed. A text separator
   needs no font to be present. */
.app-breadcrumb {
    --bs-breadcrumb-divider: "\203A"; /* › */
}

/* ---------------------------------------------------------------- code block */

.app-code-block-label {
    margin-bottom: 0.2rem;
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
}

.app-code-block-frame {
    position: relative;
}

.app-code-block-body {
    margin: 0;
    padding: 0.6rem 0.75rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
    background: var(--bs-tertiary-bg);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 0.85rem;
}

/* The block is focusable so a keyboard user can scroll and select it. */
.app-code-block-body:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* The copy button floats over the top-right corner; the body reserves room for it so a long
   single-line value (a token, an invitation link) never runs underneath it. */
.app-code-block-frame:has(.app-code-block-copy) .app-code-block-body {
    padding-right: 3rem;
}

.app-code-block-copy {
    position: absolute;
    top: 0.3rem;
    right: 0.3rem;
}

/* ---------------------------------------------------------------- radio group */

.app-radio-group {
    min-width: 0;
    padding: 0;
    border: 0;
}

.app-radio-group-legend {
    float: none; /* Bootstrap floats legends; that breaks the fieldset's own layout. */
    width: auto;
    margin-bottom: 0.4rem;
    padding: 0;
    font-size: 1rem;
    font-weight: 500;
}

/* The options are DxRadioGroup's (a dxbl-check per item, carrying ItemCssClass); the theme already spaces
   them vertically. Text over description must hold under every DevExpress 26.1.3 theme, and each one styles
   the option's label differently: the Bootstrap themes set `.dxbl-checkbox > .dxbl-text { display:
   inline-block }` (0,2,0), which outranked the old `option > label` flex column, and Fluent leaves `.dxbl-text
   { display: inline-flex; align-items: center }`, which lays block children side by side (NEXER-1382). So the
   label gets a column of its own, keyed on two classes plus .dxbl-text (0,3,0) to outrank both, and the spans
   are blocks as well, so they stack even if a theme ever outranks the label rule. Items start at the top so
   the radio stays level with the title's first line. */
.dxbl-checkbox.app-radio-option > .dxbl-text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

.app-radio-option-text,
.app-radio-option-description {
    display: block;
}

.app-radio-option-description {
    margin-top: 0.125rem;
    color: var(--bs-secondary-color);
    font-size: 0.85rem;
    line-height: 1.35;
}

/* ---------------------------------------------------------------- AppNavTabs

   One row that scrolls, rather than a block that wraps. An area with a dozen sections wrapped into a second
   row of tabs, which reads as two separate strips and pushes the page content down; scrolling keeps the
   strip a strip. The scrollbar is hidden because the tabs themselves are the affordance, and the row still
   responds to a trackpad, a shift-wheel and a keyboard. */
.nav-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
}

.nav-tabs::-webkit-scrollbar {
    display: none;
}

.nav-tabs .nav-link {
    white-space: nowrap;
}

/* AppTabs is a DxTabs. It fills its column so a grid inside a tab gets the full width, and the content
   panel is allowed to shrink below its content so that grid can scroll instead of pushing the page wide. */
.app-tabs {
    width: 100%;
    min-width: 0;
    align-self: stretch;
}

.app-tabs .dxbl-tabs-content-panel {
    min-width: 0;
}

/* ---------------------------------------------------------------- text box with buttons */

/* An AppTextBox with Buttons renders as a Bootstrap input-group around the DevExpress editor's root. The
   field keeps its rounded leading edge, the buttons keep the trailing one, and the group fills the field's
   column rather than the whole row. Bootstrap only knows to stretch a .form-control, so the editor root is
   given the same flex treatment here. */
.app-textbox-group {
    flex-wrap: nowrap;
}

.app-textbox-group > .dxbl-text-edit {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
    border-start-end-radius: 0;
    border-end-end-radius: 0;
}

/* The focus ring has to paint over the adjacent button, which Bootstrap lifts to z-index 2. */
.app-textbox-group > .dxbl-text-edit:focus-within {
    z-index: 5;
}

/* An invalid field still owns the group's border colour, so the error state does not stop at the button. */
.app-textbox-group > .dxbl-text-edit.invalid ~ .btn {
    border-color: var(--bs-form-invalid-border-color);
}

/* ---------------------------------------------------------------- duration box */

/* AppDurationBox: a DxTextBox (free text in the house duration format) with a drop-down helper of quick
   picks and four spinners. The readout under the field is the parsed value in words; it is muted like help
   text and keeps a line's height while empty so the form does not jump as the user types. */
.app-durationbox-readout {
    min-height: 1.5em;
    margin-top: 0.25rem;
}

.app-durationbox-panel {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    min-width: 16rem;
}

.app-durationbox-presets {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
}

/* Each column is at least as wide as its label, so a long unit name ("Klukkustundir") widens the panel instead
   of running into the next one; the spinner inside may shrink to the column, never the label to the spinner. */
.app-durationbox-parts {
    display: grid;
    grid-template-columns: repeat(4, minmax(min-content, 1fr));
    gap: 0.5rem 0.75rem;
}

.app-durationbox-part {
    min-width: 4.5rem;
}

.app-durationbox-part .form-label {
    display: block;
    margin-bottom: 0.125rem;
    font-size: 0.8125rem;
    white-space: nowrap;
}

.app-durationbox-part .dxbl-spin-edit,
.app-durationbox-part input {
    min-width: 0;
    width: 100%;
}

.app-durationbox-actions {
    display: flex;
    justify-content: flex-end;
}

/* ---------------------------------------------------------------- drawer form */

/* An App* editor inside a DxFormLayout is not a DxFormLayoutItem: the layout gives it its content width and
   flows the next one alongside, so three fields end up in a ragged row with their labels at different
   heights. In a drawer a field is full width, one per row. DxFormLayoutItem children keep their own spans —
   this only claims the fields the layout has no opinion about. */
.app-drawer-form > .app-field,
.app-drawer-form > .app-code-block,
.app-drawer-form > .app-drawer-form-actions {
    flex: 0 0 100%;
    max-width: 100%;
    width: 100%;
}

.app-drawer-form > .app-field {
    margin-bottom: 0.75rem;
}

/* ---------------------------------------------------------------- tabs (left rail) */

/* A left rail reads better than a horizontal strip when each tab holds a table of its own: the tables all
   start at the same place, and the eye does not have to travel back up to see which one it is looking at.
   DxTabs draws the rail itself (TabsPosition.Left) and AppTabs flips it back to Top under 768px through a
   DxLayoutBreakpoint, so the old flex layout and media query are gone; what is left is the floor that
   stops a rail of short captions from collapsing into a sliver. */
.app-tabs-left .dxbl-tabs-tablist {
    min-width: 11rem;
}

/* A hint that belongs to the field above it — a live address preview, say — and not to the form layout's
   idea of a column. */
.app-drawer-form > .form-text {
    flex: 0 0 100%;
    max-width: 100%;
    margin-top: -0.5rem;
    margin-bottom: 0.75rem;
}

/* ---------------------------------------------------------------- state blocks */

/* Loading, empty, error and information all look the same: a centred line of text with, at most, one thing
   to do about it. Only the information kind was ever styled, so an empty grid's "New" button inherited the
   grid's empty-data area instead — stretched to a slab, its icon wrapped onto its own line. */
.app-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 2rem 1.5rem;
    color: var(--bs-secondary-color);
    text-align: center;
}

/* The error kind carries a danger-coloured icon so a failed load does not read as an empty list. */
.app-state.status-error > i {
    color: var(--bs-danger, #dc3545);
    font-size: 1.5rem;
}

.app-state-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.75rem;
}

/* A button in a state block is a button, not a panel: it takes the width of its own label. */
.app-state-actions .btn,
.app-state-actions .dxbl-btn {
    width: auto;
    flex: 0 0 auto;
}

/* AppErrorMessage — the ids under an error read as a label/value list, each value with its copy button
   beside it, so the reader can see exactly what they are about to paste into a support message. */
.app-error-message-references {
    display: grid;
    gap: 0.25rem;
    margin: 0.5rem 0 0;
}

.app-error-message-reference {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem 0.5rem;
}

.app-error-message-reference dt {
    font-weight: 600;
}

.app-error-message-reference dd {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    min-width: 0;
    margin: 0;
}

.app-error-message-reference code {
    overflow-wrap: anywhere;
}

.app-error-message-copy-all {
    margin-top: 0.5rem;
}

.app-error-message-filter {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.5rem;
    margin-top: 0.5rem;
}

.app-error-message-filter code {
    overflow-wrap: anywhere;
    user-select: all;
}

/* The grid's empty area blows every icon inside it up to 3rem — meant for a big "nothing here" glyph, but it
   catches the icon on our New button too, which is why the button came out a 62px slab with its icon stacked
   above the label. The button's icon is the size of its text, wherever the button happens to be. */
.dxbl-grid .dxbl-grid-empty-data-area .app-state-actions .dxbl-image {
    width: 1em;
    height: 1em;
}

/* AppPhoneNumber / AppEmailAddress — the value and its optional copy button read as one unit, so they share a gap rather
   than inheriting whatever spacing the surrounding flex container happens to use. */
.app-contact {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    max-width: 100%;
}

.app-contact-content {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
}

/* A dialable number is not body copy: underline only on hover, so a card full of contact details does not
   read as a wall of links. */
.app-contact-link {
    color: inherit;
    text-decoration: none;
}

.app-contact-link:hover,
.app-contact-link:focus-visible {
    text-decoration: underline;
}

/* A number must not wrap mid-value — a broken number is unreadable and un-dialable by eye, and a
   long address must ellipsize rather than widen its card. */
.app-contact-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Icon-only rendering drops the text span entirely, so the content gap has nothing left to separate and
   would otherwise sit as dead space inside the hit area. */
.app-contact-icon-only {
    gap: 0;
}

/* The several-values picker is a DevExpress dropdown button, which arrives with a button's padding, width
   and colour. Inside a line of contact details it has to read as the same inline affordance the
   single-value branch renders — a customer with two numbers looking structurally different from one with
   a single number is the opposite of what the picker is for.

   Both the bare and the descendant selector are given because DevExpress puts CssClass on the button
   element itself in some render paths and on a wrapper in others; matching only one silently styles
   nothing. */
.app-contact-picker,
.app-contact-picker .dxbl-btn,
.app-contact-picker.app-dropdown-icon-only {
    width: auto;
    min-width: 0;
    padding: 0;
    border: 0;
    background-color: transparent;
    color: inherit;
    gap: 0.375rem;
}

.app-contact-picker:hover,
.app-contact-picker:focus-visible,
.app-contact-picker .dxbl-btn:hover,
.app-contact-picker .dxbl-btn:focus-visible {
    background-color: transparent;
    text-decoration: underline;
}

/* The caret is the only thing telling the user a second number exists, so it keeps its own width back —
   app-dropdown-icon-only fixes it at 2rem for toolbar buttons, which here would pad it away from the icon
   it belongs to. */
.app-contact-picker .dxbl-btn-dropdown-toggle {
    width: auto;
    min-width: 0;
    opacity: 0.7;
}

/* The sort column picker and its direction toggle are one control, so they share a row and the picker is
   wide enough for a real column caption rather than an ellipsis. */
.app-grid-ribbon-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.app-grid-ribbon-sort-box {
    min-width: 11rem;
}

/* Placeholder cards. The shimmer is opacity-only so it costs no layout, and it is disabled outright for
   users who asked for reduced motion — a pulsing grid is exactly the kind of thing that setting is for. */
.app-grid-card--skeleton {
    gap: 0.6rem;
    padding: 1rem;
    pointer-events: none;
}

.app-grid-card-skeleton-line {
    display: block;
    height: 0.75rem;
    border-radius: 0.25rem;
    background-color: var(--bs-secondary-bg, rgba(0, 0, 0, 0.08));
    animation: app-grid-card-skeleton-pulse 1.4s ease-in-out infinite;
}

.app-grid-card-skeleton-line--title {
    width: 60%;
    height: 1rem;
}

.app-grid-card-skeleton-line--short {
    width: 35%;
}

@keyframes app-grid-card-skeleton-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
    .app-grid-card-skeleton-line {
        animation: none;
    }
}

/* The selection checkbox sits in the card's top-right corner and stays out of the content flow, so a card
   template does not have to reserve space for it. It only appears on hover or once checked: a grid of
   always-visible checkboxes reads as a form, not a list. */
.app-grid-card-select {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 1;
    opacity: 0;
    transition: opacity 0.12s ease-in-out;
}

.app-grid-card {
    position: relative;
}

/* Reserve the corner the checkbox occupies. It is absolutely positioned top-right, and that is exactly
   where a card template puts its status badge — so the two overlapped and the badge sat under the box.
   Neither knows about the other: the checkbox belongs to the card view, the badge to the consumer's
   template. The header region is the agreed place for a title and a status, so the reservation goes
   there and every card using the region gets it without knowing why. */
.app-grid-card--selectable .app-grid-card-header {
    padding-right: 1.75rem;
}

.app-grid-card:hover .app-grid-card-select,
.app-grid-card:focus-within .app-grid-card-select,
.app-grid-card--selected .app-grid-card-select {
    opacity: 1;
}

/* Keyboard users never hover, so focus must reveal it too — and a checked box must stay visible even when
   the pointer leaves, or the user cannot tell what is selected. */
@media (hover: none) {
    .app-grid-card-select {
        opacity: 1;
    }
}

/* While Select is on, every box stays out. Hover-reveal is right for the resting state — a wall of cards
   should not be a wall of checkboxes — but once the user has said they are picking records, having to
   hunt for each box by hovering is the same undiscoverable gesture in a smaller form. */
.app-grid-card-view--selecting .app-grid-card-select {
    opacity: 1;
}

/* Realtime row presentation (NEXER-315).
   A deleted row is greyed and struck rather than removed: pulling it out immediately shifts every row
   beneath it under the user's cursor mid-click, and leaving it untouched lets them act on a record that
   is gone. It disappears naturally on the next fetch. */
.app-grid-row--deleted > td {
    opacity: 0.55;
    text-decoration: line-through;
}

/* The "N new" chip. Creates are buffered behind it because in server mode a new row may not belong on
   the page being viewed — the server decides the order — so the user picks when the list moves. */
.app-grid-incoming {
    display: flex;
    justify-content: center;
    padding: 0.25rem 0;
}

/* Default card (AppGrid with no CardTemplate). Label/value rows built from the grid's visible columns,
   so the card follows the column picker. The label is fixed-width and the value truncates: without it a
   long value wraps and the card stops aligning with its neighbours, which is what the region classes
   above exist to prevent. */
.app-grid-card-default-label {
    flex: 0 0 auto;
    min-width: 6rem;
    opacity: 0.75;
}

.app-grid-card-default-value {
    flex: 1 1 auto;
    min-width: 0;
}

/* A designed CardTemplate almost always wraps its body in an AppCard; that inner .card needs to shrink
   too, or the track still sizes to its min-content and the row shows fewer, wider cards than the layout
   asked for. */
.app-grid-card > .card,
.app-grid-card .card-body {
    min-width: 0;
}

/* AppScheduler — day-scale timeline headers. The grid paints the cells; these only shape the two-line
   day header (weekday over day number) and mark off-days, holidays and today so a fortnight reads at
   a glance without any host CSS. */
.app-scheduler-month-head {
    font-weight: 600;
    font-size: 0.8125rem;
    text-align: center;
}

.app-scheduler-day-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    line-height: 1.15;
    padding: 0.125rem 0;
}

.app-scheduler-day-head-dow {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    opacity: 0.7;
}

.app-scheduler-day-head-num {
    font-size: 0.875rem;
    font-weight: 600;
}

.app-scheduler-day-head-off {
    opacity: 0.55;
}

.app-scheduler-day-head-holiday .app-scheduler-day-head-num {
    color: var(--bs-danger, #dc3545);
}

.app-scheduler-day-head-today .app-scheduler-day-head-num {
    color: var(--bs-primary, #0d6efd);
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

/* AppScheduler — the optional resource rail. The grid's own navigator is a flat drop-down, so a
   grouped board needs a Team → Member tree beside it; this only lays the two out and tints the
   member swatch. Everything inside the tree is the host's own captions. */
.app-scheduler-shell {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    min-width: 0;
}

.app-scheduler-shell-board {
    flex: 1 1 auto;
    min-width: 0;
}

.app-scheduler-shell-filter {
    flex: 0 0 auto;
    width: 15rem;
    max-width: 40%;
}

.app-scheduler-resource-filter-title {
    font-weight: 600;
    font-size: 0.8125rem;
    margin-bottom: 0.25rem;
}

.app-scheduler-resource-filter-group {
    font-weight: 600;
}

.app-scheduler-resource-filter-member {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
}

.app-scheduler-resource-filter-swatch {
    display: inline-block;
    width: 0.625rem;
    height: 0.625rem;
    border-radius: 0.1875rem;
    flex: 0 0 auto;
}

@media (max-width: 575.98px) {
    .app-scheduler-shell {
        flex-direction: column;
    }

    .app-scheduler-shell-filter {
        width: 100%;
        max-width: none;
    }
}

/* Saved grid views (NEXER-714): AppViewPicker and the active-view chip. */
.app-view-picker-body {
    width: 100%;
    overflow-x: hidden;
    max-height: 26rem;
    overflow-y: auto;
}

.app-view-picker-section {
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    padding: 0.35rem 0.15rem 0.2rem;
}

.app-view-picker-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    width: 100%;
    padding: 0.3rem 0.35rem;
    border: 0;
    border-radius: 0.25rem;
    background: transparent;
    color: inherit;
    text-align: start;
}

.app-view-picker-row:hover:not(:disabled),
.app-view-picker-row.is-active {
    background: var(--bs-tertiary-bg);
}

.app-view-picker-row:disabled {
    opacity: 0.6;
}

.app-view-picker-name-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-view-picker-empty,
.app-view-picker-error,
.app-view-picker-confirm,
.app-view-picker-name {
    padding: 0.35rem 0.15rem;
}

.app-view-picker-actions,
.app-view-picker-footer {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.35rem;
}

.app-view-picker-opens-with {
    flex-basis: 100%;
}

.app-grid-view-chip {
    display: inline-flex;
    align-items: center;
    max-width: 22rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-grid-view-chip-notice {
    color: var(--bs-warning-text-emphasis);
}

/* ---------------------------------------------------------------- rich text editor */

.app-rich-text-editor {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
}

.app-rich-text-editor-label {
    margin-bottom: 0;
}

/* DxHtmlEditor's link dialog (rendered at the body, so not scoped to the component) draws its "open in new window"
   checkbox at 20 px. WCAG 2.2 AA 2.5.8 wants a 24 px target; the input is the visible box, so it grows. */
.dxbl-html-editor-modal-dialog .dxbl-checkbox input[type=checkbox] {
    min-width: 24px;
    min-height: 24px;
}

/* ---------------------------------------------------------------- html editor */

.app-html-editor {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.app-html-editor-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.app-html-editor-modes {
    display: inline-flex;
    gap: 0.25rem;
}

.app-html-editor-source {
    width: 100%;
    padding: 0.6rem 0.75rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    /* Source code, not an identifier, so it stays out of the .app-identifier group. */
    font-family: var(--bs-font-monospace, monospace);
    font-size: 0.85rem;
    line-height: 1.45;
    tab-size: 2;
    resize: vertical;
}

.app-html-editor-source:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

.app-html-editor-picker {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    max-height: 22rem;
    overflow-y: auto;
    padding: 0.5rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
    background: var(--bs-tertiary-bg);
}

.app-html-editor-picker-filter {
    width: 100%;
    padding: 0.3rem 0.5rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
}

.app-html-editor-picker-group-title {
    margin-bottom: 0.2rem;
    color: var(--bs-secondary-color);
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.app-html-editor-picker-item {
    display: flex;
    width: 100%;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.2rem 0.4rem;
    border: 0;
    border-radius: var(--bs-border-radius-sm);
    background: transparent;
    color: var(--bs-body-color);
    text-align: start;
}

.app-html-editor-picker-item {
    cursor: pointer;
}

.app-html-editor-picker-item:hover,
.app-html-editor-picker-item:focus-visible,
.app-html-editor-picker-item.is-active {
    background: var(--bs-secondary-bg);
}

/* The keyboard's active entry (aria-activedescendant): focus stays in the filter box, so mark it visibly. */
.app-html-editor-picker-item.is-active {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}

.app-html-editor-picker-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.app-html-editor-picker-text {
    display: flex;
    min-width: 0;
    flex-direction: column;
}

.app-html-editor-picker-description {
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
}

/* AppTemplatePicker: a DevExpress combo beside a subject, plain-text or SMS field. */
.app-template-picker {
    display: inline-flex;
    min-width: 16rem;
}

.app-template-picker-combo {
    width: 100%;
}

.app-template-picker-item {
    display: flex;
    width: 100%;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
}

.app-template-picker-text {
    display: flex;
    min-width: 0;
    flex-direction: column;
}

.app-template-picker-description {
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
}

.app-template-picker-path {
    font-size: 0.8rem;
    overflow-wrap: anywhere;
}

.app-html-editor-picker-path {
    font-size: 0.8rem;
    overflow-wrap: anywhere;
}

.app-html-editor-picker-empty {
    margin: 0;
    color: var(--bs-secondary-color);
}

/* AppKanban uses DevExpress grids only for their accessible drag handles, selection and virtual
   windows. The board chrome remains theme-neutral and follows Bootstrap variables supplied by the host. */
.app-kanban {
    position: relative;
    min-width: 0;
}

.app-kanban-busy {
    position: absolute;
    z-index: 3;
    top: 0.5rem;
    right: 0.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.375rem;
    border-radius: 999px;
    background: var(--bs-body-bg, #fff);
    box-shadow: var(--bs-box-shadow-sm, 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075));
    pointer-events: none;
}

.app-kanban-surface {
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    padding-bottom: 0.25rem;
}

.app-kanban-layout-grid {
    min-width: max-content;
    --dxbl-grid-text-cell-padding-x: 0.5rem;
    --dxbl-grid-text-cell-padding-y: 0;
    --dxbl-grid-header-font-weight: 600;
}

.app-kanban-layout-grid > .dxbl-scroll-viewer > .dxbl-scroll-viewer-content > .dxbl-grid-table {
    border: 0;
    border-collapse: separate;
    border-spacing: 0.5rem 0;
}

.app-kanban-layout-header-cell,
.app-kanban-layout-data-cell {
    border: 0;
    background: transparent;
    vertical-align: top;
}

.app-kanban-header {
    margin-bottom: 0.75rem;
}

.app-kanban-footer {
    margin-top: 0.75rem;
}

.app-kanban-layout-header-cell {
    padding-bottom: 0.5rem;
}

.app-kanban-layout-data-cell {
    height: var(--app-kanban-height);
}

.app-kanban-column-header,
.app-kanban-column-heading,
.app-kanban-card-move-actions,
.app-kanban-column-order-actions {
    display: flex;
    align-items: center;
}

.app-kanban-column-header {
    justify-content: space-between;
    gap: 0.5rem;
    min-width: 0;
}

.app-kanban-column-heading {
    gap: 0.5rem;
    min-width: 0;
}

.app-kanban-column-caption {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-kanban-column-count,
.app-kanban-drag-count {
    display: inline-grid;
    min-width: 1.5rem;
    min-height: 1.5rem;
    place-items: center;
    padding-inline: 0.35rem;
    border-radius: 999px;
    background: var(--bs-secondary-bg, #e9ecef);
    color: var(--bs-secondary-color, #6c757d);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
}

.app-kanban-column-loading {
    display: inline-flex;
    align-items: center;
    color: var(--bs-secondary-color, #6c757d);
}

.app-kanban-column-order-actions,
.app-kanban-card-move-actions {
    gap: 0.25rem;
}

.app-kanban-column {
    display: flex;
    height: var(--app-kanban-height);
    min-height: 12rem;
    flex-direction: column;
    overflow: hidden;
    border: var(--bs-border-width, 1px) solid var(--bs-border-color, #dee2e6);
    border-radius: var(--bs-border-radius-lg, 0.5rem);
    background: var(--bs-tertiary-bg, #f8f9fa);
}

.app-kanban-column-grid {
    min-height: 0;
    flex: 1 1 auto;
    border: 0;
    background: transparent;
    --dxbl-grid-text-cell-padding-x: 0.375rem;
    --dxbl-grid-text-cell-padding-y: 0.375rem;
}

.app-kanban-column-grid .dxbl-grid-table {
    border: 0;
    border-collapse: separate;
    border-spacing: 0 0.5rem;
    background: transparent;
}

.app-kanban-column-grid .dxbl-grid-header-row {
    display: none;
}

.app-kanban-column-grid .dxbl-scroll-viewer,
.app-kanban-column-grid .dxbl-grid-table tr,
.app-kanban-column-grid .dxbl-grid-table td {
    background: transparent;
}

.app-kanban-card-cell {
    min-width: 0;
    border-block: 0;
    background: transparent;
    vertical-align: top;
}

.app-kanban-card {
    position: relative;
    min-width: 0;
    padding: 0.75rem;
    border: var(--bs-border-width, 1px) solid var(--bs-border-color, #dee2e6);
    border-radius: var(--bs-border-radius, 0.375rem);
    background: var(--bs-body-bg, #fff);
    box-shadow: var(--bs-box-shadow-sm, 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075));
    color: var(--bs-body-color, #212529);
}

.app-kanban-card-selected {
    border-color: var(--bs-primary, #0d6efd);
    box-shadow: 0 0 0 0.125rem color-mix(in srgb, var(--bs-primary, #0d6efd) 24%, transparent);
}

.app-kanban-card-content {
    min-width: 0;
    overflow-wrap: anywhere;
}

.app-kanban-card-move-actions {
    justify-content: flex-end;
    margin-top: 0.5rem;
    opacity: 0;
    transition: opacity 120ms ease-in-out;
}

.app-kanban-card:hover .app-kanban-card-move-actions,
.app-kanban-card:focus-within .app-kanban-card-move-actions {
    opacity: 1;
}

.app-kanban-drag-anchor {
    width: 2rem;
    cursor: grab;
    border-block: 0;
    background: transparent;
}

.app-kanban-drag-anchor:active {
    cursor: grabbing;
}

.app-kanban-drag-hint-card {
    position: relative;
    width: min(18rem, 80vw);
    padding: 0.75rem;
    border: var(--bs-border-width, 1px) solid var(--bs-primary, #0d6efd);
    border-radius: var(--bs-border-radius, 0.375rem);
    background: var(--bs-body-bg, #fff);
    box-shadow: var(--bs-box-shadow, 0 0.5rem 1rem rgba(0, 0, 0, 0.15));
    color: var(--bs-body-color, #212529);
}

.app-kanban-drag-hint {
    width: min(18rem, 80vw);
    padding: 0;
}

.app-kanban-drag-hint .dxbl-grid-row-drag-hint-drag-anchor {
    display: none;
}

.app-kanban-drag-count {
    position: absolute;
    top: -0.625rem;
    right: -0.625rem;
    background: var(--bs-primary, #0d6efd);
    color: var(--bs-white, #fff);
}

.app-kanban-empty-column {
    min-height: 8rem;
}

.app-kanban-column-footer {
    flex: 0 0 auto;
    padding: 0.625rem 0.75rem;
    border-top: var(--bs-border-width, 1px) solid var(--bs-border-color, #dee2e6);
    background: var(--bs-body-bg, #fff);
}

.app-kanban-disabled {
    opacity: 0.72;
}

@media (hover: none) {
    .app-kanban-card-move-actions {
        opacity: 1;
    }
}

@media (prefers-reduced-motion: reduce) {
    .app-kanban-card-move-actions {
        transition: none;
    }
}

/* ---- Compact ribbon and selection bar (AppRibbonCompactBar, AppGridSelectionBar, below 768 px) ----
   NEXER-1251. One row: search takes the remaining width, then New, then the "⋯" overflow. Every tap target
   is at least 44px. The overflow is inline markup capped to the viewport so it never scrolls the page
   sideways; the selection bar is pinned to the bottom of the viewport. */
.app-ribbon-compact {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    min-width: 0;
    padding: 0.5rem 0;
}

.app-ribbon-compact-search,
.app-ribbon-compact-spacer {
    flex: 1 1 auto;
    min-width: 0;
}

.app-ribbon-compact-search-box {
    width: 100%;
    min-height: 44px;
}

.app-ribbon-compact .dxbl-btn,
.app-grid-selection-bar .dxbl-btn {
    min-width: 44px;
    min-height: 44px;
    flex: 0 0 auto;
}

/* The compact bar's New (NEXER-1333): an icon-only plain button the size of the "⋯" toggle — no
   background or border at rest, a primary glyph, a tint on hover and the toggle's focus ring. Three
   classes to clear DevExpress's `.dxbl-btn.dxbl-btn-text-primary` (0-2-0), as the copy button does. The
   44px box from `.app-ribbon-compact .dxbl-btn` is the touch target; padding is left to DevExpress so
   the button does not change size under the cursor. */
.app-ribbon-compact .app-ribbon-compact-primary.dxbl-btn {
    --dxbl-btn-bg: transparent;
    --dxbl-btn-border-color: transparent;
    --dxbl-btn-color: var(--bs-primary);
    --dxbl-btn-hover-color: var(--bs-primary);
    --dxbl-btn-active-color: var(--bs-primary);
    --dxbl-btn-hover-border-color: transparent;
    --dxbl-btn-active-border-color: transparent;
    --dxbl-btn-hover-bg: var(--bs-tertiary-bg);
    --dxbl-btn-active-bg: var(--bs-secondary-bg);
    align-self: center;
    justify-content: center;
    border-radius: var(--bs-border-radius);
    color: var(--bs-primary);
}

.app-ribbon-compact .app-ribbon-compact-primary.dxbl-btn:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
    box-shadow: none;
}

.app-ribbon-menu-host {
    position: relative;
    flex: 0 0 auto;
    align-self: center;
}

.app-ribbon-menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1040;
}

.app-ribbon-menu {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + 0.25rem);
    z-index: 1041;
    width: max-content;
    min-width: 14rem;
    max-width: calc(100vw - 2rem);
    max-height: 70dvh;
    overflow-x: hidden;
    overflow-y: auto;
    padding: 0.25rem 0;
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    box-shadow: var(--bs-box-shadow);
}

/* Opened from the selection bar at the bottom of the screen, the menu opens upwards. */
.app-grid-selection-bar .app-ribbon-menu {
    top: auto;
    bottom: calc(100% + 0.25rem);
}

.app-ribbon-menu-group + .app-ribbon-menu-group {
    border-top: var(--bs-border-width) solid var(--bs-border-color);
    margin-top: 0.25rem;
    padding-top: 0.25rem;
}

.app-ribbon-menu-heading,
.app-ribbon-menu-subheading {
    padding: 0.25rem 1rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
    overflow-wrap: anywhere;
}

.app-ribbon-menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background-color: transparent;
    color: var(--bs-body-color);
}

/* In the compact bar "⋯" sits beside the icon-only New, so it is bare at rest too (NEXER-1333): the pair
   reads as two plain icon buttons. The hover/expanded tint and the focus ring below still show it. */
.app-ribbon-compact .app-ribbon-menu-toggle {
    border-color: transparent;
}

.app-ribbon-menu-toggle:hover,
.app-ribbon-menu-toggle[aria-expanded="true"] {
    background-color: var(--bs-tertiary-bg);
}

.app-ribbon-menu-toggle:focus-visible,
.app-ribbon-menu .app-ribbon-menu-item:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}

.app-ribbon-menu .app-ribbon-menu-item {
    display: flex;
    align-items: center;
    width: 100%;
    min-height: 44px;
    padding: 0.5rem 1rem;
    text-align: start;
    white-space: normal;
    overflow-wrap: anywhere;
    border: 0;
    border-radius: 0;
    background-color: transparent;
    color: var(--bs-body-color);
}

.app-ribbon-menu .app-ribbon-menu-item:hover:not(:disabled),
.app-ribbon-menu .app-ribbon-menu-item:focus-visible {
    background-color: var(--bs-tertiary-bg);
}

.app-ribbon-menu .app-ribbon-menu-item:disabled,
.app-ribbon-menu .app-ribbon-menu-heading-disabled {
    color: var(--bs-secondary-color);
    opacity: 0.65;
}

.app-ribbon-menu .app-ribbon-menu-item.app-ribbon-item-danger {
    color: var(--bs-danger);
}

.app-ribbon-menu .app-ribbon-menu-separator {
    border-top: var(--bs-border-width) solid var(--bs-border-color);
}

.app-grid-selection-bar {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 1030;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    padding: 0.5rem 1rem calc(0.5rem + env(safe-area-inset-bottom, 0px));
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
    border-top: var(--bs-border-width) solid var(--bs-border-color);
    box-shadow: var(--bs-box-shadow-sm);
}

.app-grid-selection-bar-count {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.app-grid-selection-bar-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex: 0 0 auto;
}

/* The column-chooser FAB shares the bar's corner and z-index; lift it above the bar while it shows. */
.app-grid-fab.app-grid-fab--above-selection-bar {
    bottom: calc(1rem + 60px + env(safe-area-inset-bottom, 0px));
}

.app-ribbon-compact-chips {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    min-width: 0;
    padding-bottom: 0.25rem;
    white-space: nowrap;
    overflow: hidden;
}

.app-ribbon-compact-chips > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Keep the last rows and the pager clear of the pinned bar. */
.app-grid:has(.app-grid-selection-bar) {
    padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px));
}

/* "All commands": the desktop ribbon in a bottom sheet collapses within the sheet's width (its own
   adaptivity) and the sheet scrolls vertically, never sideways. */
.app-ribbon-all-commands .app-grid-ribbon {
    width: 100%;
    max-width: 100%;
    min-width: 0;
}

/* ---------------------------------------------------------------- AppTreeView (NEXER-1355)

   Figma AppTreeView/Node (145:3490): 28 px rows, 24 px of indent per level, a 12 px chevron slot, the count
   badge on primary-subtle (on the surface colour when the node is selected) and a dashed focus-coloured
   outline while something is dragged over a drop target. DevExpress draws the rows, so most of this is its
   own custom properties pointed at the NexerIQ tokens, with Bootstrap fallbacks for a host without them.

   They are set on .dxbl-treeview itself, under the wrapper's class: DevExpress declares the same properties on that
   element (its selection colour is white), and a declaration there beats one inherited from the wrapper — measured
   in a browser, the selected node rendered white text on the selected-subtle background. The row is 28 px: 20 px of
   line, 2 px of padding and DevExpress's 2 px focus border above and below. */
.app-tree-view {
    color: var(--nexeriq-text, var(--bs-body-color));
}

.app-tree-view .dxbl-treeview {
    --dxbl-treeview-font-size: 0.875rem;
    --dxbl-treeview-line-height: 1.25rem;
    --dxbl-treeview-text-padding-y: 0.125rem;
    --dxbl-treeview-item-spacing-y: 0;
    --dxbl-treeview-item-content-indent: 1.5rem;
    --dxbl-treeview-image-spacing: var(--nexeriq-space-2, 0.5rem);
    --dxbl-treeview-item-border-radius: var(--nexeriq-radius-md, var(--bs-border-radius));
    --dxbl-treeview-item-hover-bg: var(--nexeriq-hover, var(--bs-tertiary-bg));
    --dxbl-treeview-item-hover-color: var(--nexeriq-text, var(--bs-body-color));
    --dxbl-treeview-item-selection-bg: var(--nexeriq-selected, var(--bs-primary-bg-subtle));
    --dxbl-treeview-item-selection-color: var(--nexeriq-primary-text, var(--bs-primary-text-emphasis));
    --dxbl-treeview-item-focus-outline-color: var(--nexeriq-focus, var(--bs-primary));
}

.app-tree-view-node {
    display: flex;
    align-items: center;
    gap: var(--nexeriq-space-2, 0.5rem);
    width: 100%;
    min-width: 0;
    border-radius: var(--nexeriq-radius-md, var(--bs-border-radius));
}

/* A drag crossing the icon or the badge would otherwise leave the node it is still over. Only the default,
   decorative content: a NodeTemplate's own buttons or inputs must stay clickable. */
.app-tree-view-node > .app-tree-view-icon,
.app-tree-view-node > .app-tree-view-label,
.app-tree-view-node > .app-tree-view-count {
    pointer-events: none;
}

/* An outline, not a border, so the drop-target state does not add to the 28 px row. */
.app-tree-view-node-drop-target {
    outline: 1px dashed var(--nexeriq-focus, var(--bs-primary));
    outline-offset: -1px;
    background: var(--nexeriq-primary-subtle, var(--bs-primary-bg-subtle));
}

.app-tree-view-icon {
    flex: none;
    width: 1rem;
    text-align: center;
}

.app-tree-view-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-tree-view-count {
    flex: none;
    padding: 1px var(--nexeriq-space-2, 0.5rem);
    border-radius: var(--nexeriq-radius-sm, var(--bs-border-radius-sm));
    background: var(--nexeriq-primary-subtle, var(--bs-primary-bg-subtle));
    color: var(--nexeriq-primary-text, var(--bs-primary-text-emphasis));
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1rem;
}

.app-tree-view .dxbl-treeview-item-container.dxbl-active .app-tree-view-label {
    font-weight: 500;
}

.app-tree-view .dxbl-treeview-item-container.dxbl-active .app-tree-view-count {
    background: var(--nexeriq-surface, var(--bs-body-bg));
}

/* Row metrics. DevExpress sizes the row from its expand button and its text element (each 20 px of line plus 4 px
   padding and a 1 px border, so 30 px, which made a 32 px row). The text keeps 4 px of padding and loses the
   border; the expand button loses its padding and border but keeps a 24 px minimum, the WCAG 2.5.8 target size,
   which is wider than Figma's 12 px chevron slot. */
.app-tree-view .dxbl-treeview .dxbl-treeview-item-text-container > .dxbl-text {
    --dxbl-text-padding-y: var(--nexeriq-space-1, 0.25rem);
    --dxbl-text-border-width: 0;
}

.app-tree-view .dxbl-treeview .dxbl-treeview-item-content > .dxbl-btn {
    --dxbl-btn-padding-x: 0;
    --dxbl-btn-padding-y: 0;
    --dxbl-btn-border-width: 0;
    min-width: 1.5rem;
    min-height: 1.5rem;
}

/* Row states. DevExpress paints hover and selection with a ::before overlay (selection: the primary colour at 80 %
   opacity, measured in a browser), not with the background its custom properties set, so the overlay is removed
   inside AppTreeView and the states are painted on the row itself from the tokens. !important because DevExpress's
   overlay selectors carry up to seven classes. */
.app-tree-view .dxbl-treeview .dxbl-treeview-item-container::before {
    content: none !important;
}

.app-tree-view .dxbl-treeview .dxbl-treeview-item-container:hover {
    background-color: var(--nexeriq-hover, var(--bs-tertiary-bg));
}

.app-tree-view .dxbl-treeview .dxbl-treeview-item-container.dxbl-active {
    background-color: var(--nexeriq-selected, var(--bs-primary-bg-subtle));
    color: var(--nexeriq-primary-text, var(--bs-primary-text-emphasis));
}

.app-tree-view .dxbl-treeview .dxbl-treeview-item-container:focus-visible {
    outline: 2px solid var(--nexeriq-focus, var(--bs-primary));
    outline-offset: -2px;
}
