/* =============================================================
   Bibits – brand colours
     --bibits-red:       #E50B46
     --bibits-yellow:    #F5A800
     --bibits-blue:      #67B1E2
     --bibits-navy:      #202549
     --bibits-darkest:   #131022
     --bibits-gray:      #8784A3
   ============================================================= */

/* Signal themes — the ONE place a colour that means something is defined (Locked, Warning, Danger,
   Draft …). Imported FIRST so every component below can use the tokens. Register: Styling.md →
   "Signal themes". */
@import url('signal-themes.css');

/* Shared control shape — imported BEFORE the components that wear it (AliasInput,
   IconInput, ColorPicker) so their own rules can still override a detail. */
@import url('components/field-box.css');
@import url('components/search-match.css');
@import url('components/dropdown-select.css');
@import url('components/combo-box.css');
@import url('components/env-switcher.css');
@import url('components/searchable-select.css');
@import url('components/multi-select.css');
@import url('components/code-editor.css');
@import url('components/expression-editor.css');
@import url('components/value-list-picker.css');
@import url('components/parameter-panel.css');
@import url('components/tab-bar.css');
@import url('components/similar-grid.css');
@import url('components/add-column-wizard.css');
@import url('components/maintenance-notice.css');
@import url('components/text-input.css');
@import url('components/tooltip.css');
@import url('components/tool-toolbar.css');
@import url('components/toggle-switch.css');
@import url('components/data-grid.css');
@import url('components/chart.css');
@import url('components/chart-designer.css');
@import url('components/data-grid-ribbon.css');
@import url('components/form-grid.css');
@import url('components/cell-interaction.css');
@import url('components/dashboard.css');
@import url('components/community-navigator.css');
@import url('components/score-badge.css');
@import url('components/search-bar.css');
@import url('components/entity-chip.css');
@import url('components/entry-panel.css?v=5');
@import url('components/markdown-editor.css');
@import url('components/emoji-picker.css');
@import url('components/editor-toolbar.css');
@import url('components/fk-select.css');
@import url('components/page-bar.css');
@import url('components/spinner-overlay.css');
@import url('components/no-access-panel.css');
@import url('components/lock-badge.css');
@import url('components/abbreviation.css');
@import url('components/warning-marker.css');
@import url('components/phase-out-marker.css');
@import url('components/stat-tile.css');
@import url('components/tile-frame.css');
@import url('components/module-tiles.css');
@import url('components/rls-value-picker.css');
@import url('components/rls-recalculation.css');
@import url('components/lock-banner.css');
@import url('components/confirm-modal.css');
@import url('components/form-modal.css?v=3');
@import url('components/contract-check-dialog.css');
@import url('components/table-picker.css');
@import url('components/icon-picker.css');
@import url('components/icon-render.css');
@import url('components/entity-list.css');
@import url('components/tool-page.css');
@import url('components/segmented-control.css');
@import url('components/slider.css');
@import url('components/file-picker.css');
@import url('components/range-slider.css');
@import url('components/timeline.css');
@import url('components/timeline-bar.css');
@import url('components/color-picker.css');
@import url('components/refresh-control.css');
@import url('components/wizard.css');
@import url('components/json-editor.css');
@import url('components/alias-input.css');
@import url('components/icon-input.css');
@import url('components/api-explorer.css');
@import url('components/splitter.css');
@import url('components/master-detail.css');
@import url('components/button.css');
@import url('components/split-button.css');
@import url('components/placeholder-picker.css');
@import url('components/floating-inspector.css');
@import url('components/properties-panel.css');
@import url('components/property-grid.css');
@import url('components/detail-screen.css');
@import url('components/resize-handle.css');
@import url('components/sortable-tree.css');
@import url('components/constraint-editor.css');
@import url('components/ordered-list.css');
@import url('components/tag-editor.css');
@import url('components/filter-bar.css');
@import url('components/setting-control.css');
@import url('components/ai-assistant.css');
@import url('components/progress-ring.css');
@import url('components/progress-bar.css');
@import url('components/note.css');
@import url('components/module-help.css');
@import url('components/notebook-cell.css');
@import url('components/cell-stack.css');
@import url('components/process-tray.css');
@import url('components/activity-center.css');
@import url('components/document-upload.css');
@import url('components/document-list.css');
@import url('components/document-detail.css');
@import url('components/document-viewer.css');
@import url('components/document-picker.css');
@import url('components/image-picker.css');
@import url('components/comment-thread.css');
@import url('components/page-editor.css');
@import url('components/dock.css');
@import url('components/dock-window.css');
@import url('components/tabbed-window.css');
@import url('components/value-list-window.css');
@import url('components/draft-records.css');
@import url('components/schedule-window.css');
@import url('components/draft-lifecycle.css');
@import url('components/lineage-grid.css');
@import url('components/run-history.css');
@import url('components/flow-diagram.css');
@import url('components/table-diagram.css');
@import url('components/workflow-priority.css');
@import url('components/workflow-worklist.css');
@import url('components/workflow-task-panel.css');
@import url('components/workflow-start-button.css');
@import url('components/workitems-panel.css');

/* =============================================================
   Accessibility — skip link (WCAG 2.4.1)
   ============================================================= */
.skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 9999;
    padding: 0.5rem 1rem;
    background: #202549;
    color: #ffffff;
    font-weight: 600;
    font-size: 0.9rem;
    border-radius: 0 0 6px 0;
    text-decoration: none;
    /* Off-screen until focused */
    transform: translateY(-100%);
    transition: transform 0.15s ease;
}

.skip-link:focus-visible {
    transform: translateY(0);
    outline: 2px solid #67B1E2;
    outline-offset: 2px;
}

/* Bootstrap primary override → Bibits red */
:root {
    /* ── ONE control height, everywhere (owner, 2026-08-13 23:5x) ──────────────────────────
       ‼️ EVERY input-shaped control in the portal is --ctl-h: buttons, SearchableSelect,
       DropdownSelect, FkSelect and every field wearing the shared field box (AliasInput, IconInput,
       ColorPicker, TextInput) — in a toolbar, in a panel and in a form alike. There is no second
       value and no context exception.

       It started as two (panel 31px, form 36.5px) and the owner asked the obvious question: why not
       one? A single number cannot be applied to the wrong context, needs no .ep-field overrides —
       the very rules that broke twice while this was being built — and makes forms denser, which is
       what a screen with twenty fields wants.

       ‼️ The TYPE SIZE moves with it. 31px is Bootstrap's .btn-sm height, which carries 0.875rem
       text; the form fields' 0.9375rem left only 3px of breathing room above and below. So form
       inputs, textareas and in-form select triggers all come down to the small size in the same
       change — the height alone would have produced a cramped field, not a compact one.

       31px clears the WCAG 2.5.8 (AA) 24px minimum target size. Multi-line controls (textarea, the
       markdown editor) take the TYPE size but never the height: their height is their content. */
    --ctl-h: 1.9375rem;   /* 31px — the one control height (= Bootstrap .btn-sm) */

    --bs-primary:              #E50B46;
    --bs-primary-rgb:          229, 11, 70;
    --bs-link-color:           #67B1E2;
    --bs-link-color-rgb:       103, 177, 226;
    /* ‼️ A LINK KEEPS ITS COLOUR UNDER THE POINTER (owner, 2026-10-18, OPEN-812 = A). This was the brand red #E50B46,
       so every plain link in the portal turned red when pointed at - and red signals danger: "Rood … moet niet gebruikt
       worden voor niet signalerende zaken" (frontend rules, RED SIGNALS). The pointer is shown by the underline. */
    --bs-link-hover-color:     #67B1E2;
    --bs-link-hover-color-rgb: 103, 177, 226;
}

/* Base canvas color — also applied inline by the flash-free script in _Layout.cshtml so
   redirect intermediates (e.g. /signin-oidc) don't flash white. Keep both in sync. */
:root[data-bs-theme="light"] { background-color: #ffffff; color-scheme: light; }
:root[data-bs-theme="dark"]  { background-color: #131022; color-scheme: dark; }

/* ── NO BUTTON IS FILLED — "neutral frame, coloured content" (owner, 2026-10-01, his 1.2 = C) ──────
   Every tier is drawn as an OUTLINE. All buttons share ONE neutral grey frame and say what they are
   with the colour of their TEXT and ICON; only the Primary (btn-action) and the amber Warning tier
   draw their frame in their own colour, because those are the two buttons that ask for the reader's
   next move. So the one main action on a screen is the one coloured frame on it.
     • at rest  — transparent, grey frame (coloured for Primary / Warning), coloured text;
     • hover    — a soft wash of the tier's own colour (the signal theme's -bg), never a fill;
     • disabled — a faded GREY outline (the rule further down). Never dashed: in the signal-theme
                  border vocabulary a dashed line already means "not real yet" (Draft).
   ‼️ The TEXT takes the signal theme's -fg shade, never its -solid fill colour: the action blue
   #67B1E2 is 2.34:1 on white and the amber #F5A800 2.00:1, both below WCAG AA (4.5:1). The -fg
   shades measure 4.93 (blue), 5.36 (red) and 4.93 (amber) on white. The comparison page the owner
   chose from showed this exact look on the light and the dark canvas (four proposals, 2026-10-01). */

/* Destructive — Delete / irreversible actions only. Grey frame, RED text and icon; on hover the
   frame turns red over a soft red wash.
   btn-primary is kept as a DEPRECATED ALIAS (Bootstrap name; "primary = red" is a wart):
   prefer btn-destructive and migrate consumers on touch.
   btn-outline-danger (the Button family's 'danger-outline') wears the same look: it used to be the
   quieter red beside a solid one, and with no solid red left a red FRAME would make it louder than
   [Delete] itself. */
.btn-destructive,
.btn-primary,
.btn-outline-danger {
    --bs-btn-bg:                    transparent;
    --bs-btn-border-color:          var(--btn-utility-border);
    --bs-btn-color:                 var(--signal-danger-fg);
    --bs-btn-hover-bg:              var(--signal-danger-bg);
    --bs-btn-hover-border-color:    var(--signal-danger-fg);
    --bs-btn-hover-color:           var(--signal-danger-fg);
    --bs-btn-active-bg:             color-mix(in srgb, var(--signal-danger-solid) 20%, transparent);
    --bs-btn-active-border-color:   var(--signal-danger-fg);
    --bs-btn-active-color:          var(--signal-danger-fg);
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: var(--btn-utility-border);
    --bs-btn-disabled-color:        var(--signal-danger-fg);
}

/* ── Neutral-button palette (theme-aware) ───────────────────────────────────
   The grey frame every button shares, and the text colours of btn-secondary-action and
   btn-utility. --btn-secondary-bg is the soft wash a neutral button gets on hover. */
:root,
:root[data-bs-theme="light"] {
    --btn-secondary-bg:        #E7EAF2;
    --btn-secondary-color:     #2A2F4A;
    --btn-secondary-border:    #CFD6E6;
    --btn-secondary-hover-bg:  #D9DFED;
    --btn-secondary-active-bg: #C9D1E4;
    --btn-utility-color:       #3C384F;
    --btn-utility-border:      #ADB5C4;   /* visible mid-grey border so utility reads as a button */
    --btn-frame-hover-border:  #6C757D;   /* the grey frame under the pointer */
}
:root[data-bs-theme="dark"] {
    --btn-secondary-bg:        rgba(255, 255, 255, 0.10);
    --btn-secondary-color:     #F0F0F0;
    --btn-secondary-border:    rgba(255, 255, 255, 0.20);
    --btn-secondary-hover-bg:  rgba(255, 255, 255, 0.16);
    --btn-secondary-active-bg: rgba(255, 255, 255, 0.22);
    --btn-utility-color:       #C7C4D6;
    --btn-utility-border:      rgba(255, 255, 255, 0.32);
    --btn-frame-hover-border:  #ADB5BD;
}

/* Secondary action — confirmatory / safe actions (Edit, Apply, Confirm, Close).
   Grey frame, FULL-strength text at weight 500: one step above a utility button (muted text,
   normal weight) and one step below the Primary (coloured frame and text, weight 600).
   (The legacy brand-named secondary alias was removed 2026-06-13 — all consumers migrated to
   btn-secondary-action.) */
.btn-secondary-action {
    --bs-btn-bg:                    transparent;
    --bs-btn-border-color:          var(--btn-utility-border);
    --bs-btn-color:                 var(--btn-secondary-color);
    --bs-btn-font-weight:           500;
    --bs-btn-hover-bg:              var(--btn-secondary-bg);
    --bs-btn-hover-border-color:    var(--btn-frame-hover-border);
    --bs-btn-hover-color:           var(--btn-secondary-color);
    --bs-btn-active-bg:             var(--btn-secondary-active-bg);
    --bs-btn-active-border-color:   var(--btn-frame-hover-border);
    --bs-btn-active-color:          var(--btn-secondary-color);
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: var(--btn-utility-border);
    --bs-btn-disabled-color:        var(--btn-secondary-color);
}

/* Utility — low-emphasis buttons (Filter, Sort, Columns, Reload, Cancel).
   NEUTRAL-GREY OUTLINE with MUTED text: the quietest tier. It shares its frame with Secondary and
   Destructive; what sets it apart is the text — muted and normal weight here, full-strength (500)
   on Secondary, red on Destructive. Unchanged by the 2026-10-01 outline restyle: it already was one. */
.btn-utility {
    --bs-btn-bg:                 transparent;
    --bs-btn-border-color:       var(--btn-utility-border);
    --bs-btn-color:              var(--btn-utility-color);
    --bs-btn-hover-bg:           var(--btn-secondary-bg);
    --bs-btn-hover-border-color: var(--btn-utility-border);
    --bs-btn-hover-color:        var(--btn-secondary-color);
    --bs-btn-active-bg:          var(--btn-secondary-active-bg);
    --bs-btn-active-border-color:var(--btn-utility-border);
    --bs-btn-active-color:       var(--btn-secondary-color);
    --bs-btn-disabled-opacity:   0.5;
}

/*
 * btn-action — THE primary action button on a screen.
 * The Action signal theme (signal-themes.css), the same family as selected tabs and active states:
 * frame AND text in its -fg shade (#2176AD on a light page, #67B1E2 on a dark one), weight 600.
 * It is the ONE button with a coloured frame, which is what makes it findable without a fill.
 * Use for: Add, Save (primary form), Submit — the single most important action.
 * One btn-action per screen/panel. All other confirmatory actions use btn-secondary-action.
 */
.btn-action {
    --bs-btn-bg:                    transparent;
    --bs-btn-border-color:          var(--signal-action-fg);
    --bs-btn-color:                 var(--signal-action-fg);
    --bs-btn-font-weight:           600;
    --bs-btn-hover-bg:              var(--signal-action-bg);
    --bs-btn-hover-border-color:    var(--signal-action-fg);
    --bs-btn-hover-color:           var(--signal-action-fg);
    --bs-btn-active-bg:             color-mix(in srgb, var(--signal-action-solid) 24%, transparent);
    --bs-btn-active-border-color:   var(--signal-action-fg);
    --bs-btn-active-color:          var(--signal-action-fg);
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: var(--signal-action-fg);
    --bs-btn-disabled-color:        var(--signal-action-fg);
}

/*
 * btn-warning-action - 'this went wrong and YOU must act now'.
 * The Warning signal theme: frame AND text in its -fg shade (#976700 on a light page, #F5A800 on a
 * dark one), weight 600 - like the Primary, a coloured frame, because it asks for the next move.
 * Use for: the Wizard's [Retry] after a run that failed - an action that REPEATS something the
 * platform could not finish. Never for Save, never for Delete, and never to colour a label or a
 * chip: amber on a noun reads as an alarm (see the entity-chip rule).
 * Added 2026-09-11 with the wizard [Retry] (there was no amber tier in the Button family at all).
 */
.btn-warning-action {
    --bs-btn-bg:                    transparent;
    --bs-btn-border-color:          var(--signal-warning-fg);
    --bs-btn-color:                 var(--signal-warning-fg);
    --bs-btn-font-weight:           600;
    --bs-btn-hover-bg:              var(--signal-warning-bg);
    --bs-btn-hover-border-color:    var(--signal-warning-fg);
    --bs-btn-hover-color:           var(--signal-warning-fg);
    --bs-btn-active-bg:             color-mix(in srgb, var(--signal-warning-solid) 26%, transparent);
    --bs-btn-active-border-color:   var(--signal-warning-fg);
    --bs-btn-active-color:          var(--signal-warning-fg);
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: var(--signal-warning-fg);
    --bs-btn-disabled-color:        var(--signal-warning-fg);
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.btn-warning-action:disabled,
.btn-warning-action[aria-disabled="true"] { cursor: not-allowed; }

/*
 * btn-glyph - the icon ALONE: no frame and no fill, at rest, under the pointer, pressed or disabled.
 * The GlyphButton tier (button.js). The glyph takes the secondary text colour and turns to the full text
 * colour under the pointer and while pressed - the look of the (?) (HelpTip) it stands beside in a
 * surface's head. Inside a tile both are the tile's own inks: a tile points --bs-secondary-color at its soft
 * ink (dashboard.css, reporting.css), and the body colour reads on both the black glass and the pearl.
 * Use for: an action in a surface's own head, beside its title - the pin on a report tile (owner,
 * 2026-10-17, OPEN-773 = B: "achter de titel van de tile en ZONDER kader"). Never for a toolbar.
 * It keeps the shared control height (btn-sm), so the target stays 31px; the focus ring is the family's,
 * drawn inside (the -2px icon-only rule below). Disabled = the glyph at half strength, as on the utility tier.
 */
.btn-glyph {
    --bs-btn-bg:                    transparent;
    --bs-btn-border-color:          transparent;
    --bs-btn-color:                 var(--bs-secondary-color);
    --bs-btn-hover-bg:              transparent;
    --bs-btn-hover-border-color:    transparent;
    --bs-btn-hover-color:           var(--bs-body-color);
    --bs-btn-active-bg:             transparent;
    --bs-btn-active-border-color:   transparent;
    --bs-btn-active-color:          var(--bs-body-color);
    --bs-btn-disabled-bg:           transparent;
    --bs-btn-disabled-border-color: transparent;
    --bs-btn-disabled-color:        var(--bs-secondary-color);
    --bs-btn-disabled-opacity:      0.5;
}

/* ── ONE WIDTH FOR EVERY LABELLED BUTTON (owner, 2026-09-13) ─────────────────────────────────────
   ‼️ WHY THIS IS ONE RULE ON ALL TIERS, AND NOT A min-width PER TIER. Until 2026-09-13 only the
   primary (btn-action) and the amber tier carried `min-width: 7.5rem`; Edit, Delete, Cancel and every
   utility button with a label were as narrow as their text. So [Edit] stood visibly smaller than
   [Save] beside it, on every screen. A minimum on the tier that happens to be edited last is exactly
   how that drift starts, so the minimum now lives here, once, and a parity test in
   Portal/Web.Tests (ButtonWidthContractTests) refuses a tier rule that sets its own width again.

   ‼️ A MINIMUM, NEVER A FIXED WIDTH. Labels are translated: a fixed width would cut "Speichern" or
   "Enregistrer" off, so a longer label makes its button grow. The other half of "all the same width"
   is `--btn-group-w`: buttons that stand side by side in one parent are sized to the WIDEST of them by
   the Button family (startButtonGroupSizing in button.js), which writes that variable on each button.

   NOT affected, deliberately:
     • an icon-only button (`btn-icon`, or a lone icon/spinner as its only child, or a label that is
       only there for a screen reader) — the toolbar rule makes those square, not 7.5rem wide;
     • a button inside a grid ROW — a per-row action lives in a column that must not be pushed wide;
     • anything without a tier class (btn-link, header login, the colour picker's "Use"). */
:root {
    --btn-min-w: 7.5rem;
}
@media (max-width: 575.98px) {
    :root { --btn-min-w: 5.5rem; }   /* three buttons in a footer must still fit a phone */
}

/* ‼️ ONE LINE, and it has to be: a line break before a `:not(…)` turns it into a DESCENDANT
   combinator, so the rule would size some element INSIDE the button and leave the button itself
   untouched — a wrapped selector that silently does nothing. */
.btn:is(.btn-action, .btn-secondary-action, .btn-utility, .btn-destructive, .btn-primary, .btn-warning-action, .btn-outline-danger, .btn-glyph):not(.btn-icon):not(:has(> :is(i, .bi, svg, img, .spinner-border):only-child)):not(:has(> .visually-hidden)):not([role="row"] .btn) {
    min-width:       max(var(--btn-min-w), var(--btn-group-w, 0px));
    justify-content: center;
}

/* Disabled = a faded GREY OUTLINE with muted text — the same look for every tier, so an unavailable
   [Save] and an unavailable [Delete] read alike: "not now". No fill (owner, 2026-10-01): among
   outlines a grey FILL — the look until that day — became the heaviest button on the screen, the
   opposite of inactive. Never dashed: a dashed line already means "not real yet" (Draft). Scoped to
   :disabled:NOT busy — a BUSY button (aria-busy, set by the Button component during async work)
   keeps its own frame and colour + spinner. `!important` kept as a robust guard against per-tool
   colour overrides — and it is also what holds this look under :hover and :active for the
   [aria-disabled] form below, which, unlike :disabled, still receives the pointer.
   ‼️ [aria-disabled="true"] IS THE SAME STATE, FOR A BUTTON THAT SAYS WHY (owner, 2026-10-02, 1.57 = A).
   A Button given a `disabledReason` is rendered aria-disabled instead of natively disabled, so it stays
   focusable and the shared tooltip can show the reason (button.js). It must not LOOK any different
   from its natively disabled twin, so every tier selector here is listed twice. */
.btn-action:disabled:not([aria-busy="true"]),
.btn-secondary-action:disabled:not([aria-busy="true"]),
.btn-destructive:disabled:not([aria-busy="true"]),
.btn-warning-action:disabled:not([aria-busy="true"]),
.btn-primary:disabled:not([aria-busy="true"]),
.btn-outline-danger:disabled:not([aria-busy="true"]),
.btn-action[aria-disabled="true"]:not([aria-busy="true"]),
.btn-secondary-action[aria-disabled="true"]:not([aria-busy="true"]),
.btn-destructive[aria-disabled="true"]:not([aria-busy="true"]),
.btn-warning-action[aria-disabled="true"]:not([aria-busy="true"]),
.btn-primary[aria-disabled="true"]:not([aria-busy="true"]),
/* signal-colour: names Bootstrap's btn-outline-danger CLASS for its aria-disabled form — a selector, not a colour */
.btn-outline-danger[aria-disabled="true"]:not([aria-busy="true"]) {
    background-color: transparent !important;
    border-color:     var(--btn-utility-border) !important;
    color:            var(--btn-utility-color) !important;
    opacity:          0.5 !important;
    cursor:           not-allowed;
}

/* Every OTHER .btn that is aria-disabled (the utility tier, or a .btn without a tier) takes Bootstrap's
   own disabled look — the same four values `.btn:disabled` sets — so it is indistinguishable from a
   natively disabled one at rest, under the pointer and while pressed. What it deliberately does NOT copy
   is `pointer-events: none`: the pointer must reach the button for its tooltip to open. One step more
   specific than Bootstrap's :hover / :focus-visible, and later than its :active, so those do not light it
   up. The keyboard focus ring is an OUTLINE (below) and is kept: a focusable control must show its focus. */
.btn[aria-disabled="true"]:not([aria-busy="true"]) {
    color:            var(--bs-btn-disabled-color);
    background-color: var(--bs-btn-disabled-bg);
    border-color:     var(--bs-btn-disabled-border-color);
    opacity:          var(--bs-btn-disabled-opacity);
    box-shadow:       none;
    cursor:           not-allowed;
}

/* =============================================================
   Theme colour palette
   Adjust these values to restyle the entire shell in one place.

    --color-header-bg-light:  #202549;    header bar              
    --color-sidebar-bg-light: #202549;   side navigation         
    --color-content-bg-light: #f0f0f6;    main content area      

    --color-header-bg-dark:   #131022;    header bar              
    --color-sidebar-bg-dark:  #131022;    side navigation         
    --color-content-bg-dark:  #1a1628;    main content area      
   ============================================================= */
:root {
        
    /* ── Light mode backgrounds ───────────────────────────────── */
    --color-header-bg-light:     rgb(196,196,196);              /* header bar              */
    --color-sidebar-bg-light:    rgb(196,196,196);              /* side navigation         */
    --color-content-bg-light:    #ffffff;              /* main content area       */

    /* ── Dark mode backgrounds ────────────────────────────────── */
    --color-header-bg-dark:      #181818;              /* header bar              */
    --color-sidebar-bg-dark:     #181818;              /* side navigation         */
    --color-content-bg-dark:     #212121;              /* main content area       */

    /* ── Light mode borders ───────────────────────────────────── */
    --color-header-border-light:  #E2E8F0; /* bottom border of header */
    --color-sidebar-border-light: #E2E8F0; /* right border of sidebar */

    /* ── Dark mode borders ────────────────────────────────────── */
    --color-header-border-dark:   #1e1e1e; /* bottom border of header */
    --color-sidebar-border-dark:  #1e1e1e; /* right border of sidebar */

    /* ── Light mode text ──────────────────────────────────────── */
    --color-header-text-light:   #222222;             /* header icons & labels   */
    --color-sidebar-text-light:  #222222;             /* sidebar nav labels      */
    --color-content-text-light:  #222222;             /* main content text       */

    /* ── Dark mode text ───────────────────────────────────────── */
    --color-header-text-dark:    #c4c4c4;             /* header icons & labels   */
    --color-sidebar-text-dark:   #c4c4c4;             /* sidebar nav labels      */
    --color-content-text-dark:   #e8e8e8;             /* main content text       */
}

/* =============================================================
   CSS Design Tokens
   ============================================================= */
:root {
    /* Layout */
    --header-height: 44px;   /* used only as mobile sidebar top-offset fallback */
    --sidebar-width: 240px;
    --sidebar-collapsed-width: 52px;
    --sidebar-speed: 0.22s;
    /* The tone the navy glass of a hovered or selected menu item reads as — data-grid.css --dg-card-navy over
       --dg-card-body, at its lightest end — in both themes. community-identity.js (paintCommunityIcons) makes the
       icon on that glass readable against it; the icon at rest is judged against the menu's own background. */
    --sidebar-glass-tone:   #32426e;   /* signal-colour: a measured surface tone, not a meaning */

    /* Header + Sidebar shell — light mode */
    --header-bg:         var(--color-header-bg-light);
    --header-color:      var(--color-header-text-light);
    --header-btn-hover:  rgba(255, 255, 255, 0.09);
    --header-border:     var(--color-header-border-light);

    /* Sidebar — light mode */
    --sidebar-bg:           var(--color-sidebar-bg-light);
    --sidebar-color:        var(--color-sidebar-text-light);
    --sidebar-hover-bg:     rgba(255, 255, 255, 0.07);
    --sidebar-active-bg:    rgba(103, 177, 226, 0.1);
    --sidebar-active-color: #ffffff;
    --sidebar-border:       var(--color-sidebar-border-light);
    --sidebar-icon-color:   #67B1E2;
    --sidebar-icon-active:  #67B1E2;
    --sidebar-active-accent:#67B1E2;

    /* Content — light mode */
    --content-bg:    var(--color-content-bg-light);
    --content-color: var(--color-content-text-light);
}

[data-bs-theme="dark"] {
    /* Header + Sidebar shell — dark mode */
    --header-bg:         var(--color-header-bg-dark);
    --header-color:      var(--color-header-text-dark);
    --header-border:     var(--color-header-border-dark);

    /* Sidebar — dark mode */
    --sidebar-bg:           var(--color-sidebar-bg-dark);
    --sidebar-color:        var(--color-sidebar-text-dark);
    --sidebar-hover-bg:     rgba(255, 255, 255, 0.05);
    --sidebar-active-bg:    rgba(103, 177, 226, 0.1);
    --sidebar-active-color: #ffffff;
    --sidebar-border:       var(--color-sidebar-border-dark);
    --sidebar-icon-color:   #67B1E2;
    --sidebar-icon-active:  #67B1E2;
    --sidebar-active-accent:#67B1E2;

    /* Content — dark mode */
    --content-bg:    var(--color-content-bg-dark);
    --content-color: var(--color-content-text-dark);
}

/* =============================================================
   Base reset
   ============================================================= */
*, *::before, *::after {
    box-sizing: border-box;
}

html {
    height: 100%;
    font-size: 14px;
    -webkit-text-size-adjust: 100%;
}

@media (min-width: 768px) {
    html { font-size: 15px; }
}

body {
    margin: 0;
    padding: 0;
    height: 100%;
    overflow: hidden;
    background-color: var(--content-bg);
    color: var(--content-color);
}

/* =============================================================
   Layout wrapper — full viewport, no scroll
   ============================================================= */
#layout-wrapper {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
}

/* ── The AI assistant's chat BESIDE the page (owner, 2026-10-21, his 1.13 = A; CL-806) ───────────────────────────────────
   While the chat is open and DOCKED to a side with "Beside the page" chosen in its move menu, ai-assistant.js puts
   `assistant-beside` on <html> and the room the chat takes on that side — its zone plus its splitter, in px, measured on every
   layout change, every splitter step and every window resize — in `--assistant-beside-{left|right|top|bottom}` (only that one
   side is set). The WHOLE page — header, menu and content — then stands in the rest of the window: nothing of the portal is
   under the chat, and grids and charts lay themselves out again because their box changed. The page's own overlays that stand
   at a window edge move in with it (rules at the end of the shell section, after their own). Over the page, floating, on a
   phone (the sheet) or with the chat closed the class is not there and nothing here applies. */
html.assistant-beside #layout-wrapper {
    margin: var(--assistant-beside-top, 0px) var(--assistant-beside-right, 0px)
            var(--assistant-beside-bottom, 0px) var(--assistant-beside-left, 0px);
    height: calc(100vh - var(--assistant-beside-top, 0px) - var(--assistant-beside-bottom, 0px));
    height: calc(100dvh - var(--assistant-beside-top, 0px) - var(--assistant-beside-bottom, 0px));
}

/* =============================================================
   Header
   ============================================================= */
#app-header {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Right side = the portal gutter's right edge (site.css), so the account menu ends where the page
       bar's buttons and the screen's content end (owner, 2026-09-13). The left keeps its own .625rem:
       the menu toggle there lines up with the sidebar, not with the content. */
    padding: 0.375rem var(--portal-gutter-end) 0.375rem 0.625rem;
    gap: 0.5rem;
    background-color: var(--header-bg);
    color: var(--header-color);
    border-bottom: 1px solid var(--header-border);
    z-index: 1030;
}

/* ‼️ THE START OF THE HEADER TAKES WHAT THE BUTTONS AT ITS END LEAVE, AND NOTHING MORE (owner, 2026-10-24, OPEN-1147 = A).
   It grows into the free width and shrinks below its content (`flex: 1 1 0`), and it is the CONTAINER the environment badge
   measures itself against (env-switcher.css): the community name gives way with an ellipsis, and where not even a few
   letters fit only the logo stays. Until that day it was sized by its content while `.header-end` never shrinks, so on a
   phone the badge ran on under the network button and its caret disappeared. */
.header-start {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
    flex: 1 1 0;
    container: header-start / inline-size;
}

.header-end {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex-shrink: 0;
}

/* ── ON A PHONE THE HEADER MAKES ROOM WHILE THE READER SCROLLS DOWN (owner, 2026-10-24, OPEN-1146 = 1-5) ──────────────
   scroll-chrome.js puts `chrome-scrolled` on <html> after a scroll down through the screen and takes it off on a scroll
   up, at the top, on a navigation and when the keyboard enters the header; it measures the header into --chrome-header-h,
   and the header slides out by exactly that. The page bar stays: on a phone it names the tab the reader is in (TabBar
   drill hosts, OPEN-1148). Below 576px only. */
@media (max-width: 575.98px) {
    #app-header {
        transition: margin-top 0.2s ease;
    }

    html.chrome-scrolled #app-header {
        margin-top: calc(-1 * var(--chrome-header-h, 0px));
    }
}

@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {
    #app-header {
        transition: none;
    }
}

/* =============================================================
   Tenant / environment dropdown in header-start
   ============================================================= */
/* Community + Database header selectors — the SAME DropdownSelect control, identical look,
   equal fixed width. Both are `<details class="ds-root header-*-select">`. */
#community-select.header-community-select .ds-trigger,
#database-select.header-database-select .ds-trigger {
    background: transparent;
    border-color: transparent;
    color: var(--header-color);
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    width: var(--header-select-width, 190px);
    padding: 0.35rem 0.5rem;
    border-radius: 6px;
    transition: background-color 0.15s ease;
}

#community-select.header-community-select .ds-trigger:hover,
#community-select.header-community-select[open] .ds-trigger,
#database-select.header-database-select .ds-trigger:hover,
#database-select.header-database-select[open] .ds-trigger {
    background-color: var(--header-btn-hover) !important;
    border-color: transparent;
    box-shadow: none;
}

#community-select.header-community-select .ds-trigger:focus-visible,
#database-select.header-database-select .ds-trigger:focus-visible {
    background-color: var(--header-btn-hover) !important;
    border-color: transparent;
    outline: 2px solid #67B1E2;
    outline-offset: 1px;
    box-shadow: none;
}

#community-select.header-community-select .ds-options,
#database-select.header-database-select .ds-options {
    left: 0;
    right: auto;
    min-width: 200px;
}

/* Truncate the label within the fixed-width trigger. */
#community-select.header-community-select .ds-value,
#database-select.header-database-select .ds-value {
    flex: 1;
    min-width: 0;
    max-width: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tenant-chevron {
    font-size: 0.6rem;
    opacity: 0.7;
    line-height: 1;
    display: flex;
}

.tenant-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 180px;
}

.tenant-logo {
    width: 20px;
    height: 20px;
    object-fit: contain;
    border-radius: 3px;
}

/* Show img, hide icon when logo is loaded; default: icon visible */
.tenant-logo-wrap { display: flex; align-items: center; }
.tenant-logo-wrap .tenant-logo.d-none ~ i { display: inline-block !important; }
.tenant-logo-wrap .tenant-logo:not(.d-none) ~ i { display: none !important; }

/* =============================================================
   Account / settings dropdown
   ============================================================= */
.account-menu {
    /* All menus share ONE width: --sidebar-width (sidebar, account dropdown, menu-builder tree). */
    min-width: var(--sidebar-width);
}

.account-menu .dropdown-item {
    display: flex;
    align-items: center;
}

/* Community (tenant) dropdown — Bootstrap dropdown-item needs flex for icon alignment */
#tenant-list .dropdown-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* Header buttons */
.header-btn {
    color: var(--header-color) !important;
    background: transparent !important;
    border-color: transparent !important;
    padding: 0.35rem 0.5rem;
    border-radius: 6px;
    transition: background-color 0.15s ease;
}

.header-btn:hover,
.header-btn.show,
.header-btn[aria-expanded="true"] {
    background-color: var(--header-btn-hover) !important;
    color: var(--header-color) !important;
    border-color: transparent !important;
    box-shadow: none !important;
}

/* Keyboard focus ring — separate from hover so mouse clicks don't show it (WCAG 2.4.7) */
.header-btn:focus-visible {
    background-color: var(--header-btn-hover) !important;
    color: var(--header-color) !important;
    border-color: transparent !important;
    outline: 2px solid #67B1E2;
    outline-offset: 1px;
    box-shadow: none !important;
}

/* Notification bell badge — hangs off top-right corner, never affects button width */
.notification-badge {
    position:        absolute;
    top:             14px;
    right:           4px;
    transform:       translate(50%, -50%);
    min-width:       1.4em;
    height:          1.4em;
    padding:         0 0.25em;
    font-size:       0.6rem;
    line-height:     1.4em;
    text-align:      center;
    border-radius:   1em;
    pointer-events:  none;
    white-space:     nowrap;
}

/* ‼️ The bell's count is INFORMATION, so it wears the light blue of an action — like the process tray's — and not the
   danger red a Bootstrap bg-danger gave it (owner, 2026-09-24). The inbox keeps its red: he asked for the bell. */
.notification-badge--info {
    background: var(--signal-action-solid);
    color:      var(--signal-action-on-solid);
}

/* Notification panel dropdown */
.notification-panel {
    position:   fixed !important;
    top:        3.5rem !important;
    right:      0.75rem !important;
    left:       auto !important;
    transform:  none !important;
    width:      320px;
    max-width:  calc(100vw - 1.5rem);
    max-height: calc(100vh - 4.5rem);
    padding:    0;
    overflow:   hidden;
    z-index:    1080;
    background: var(--bs-body-bg);
    border:     1px solid var(--bs-border-color);
    border-radius: 0.5rem;
    box-shadow: 0 8px 32px rgba(0,0,0,.25);
    color:      var(--bs-body-color);
    /* display is controlled by Bootstrap (.show adds display:block);
       we override it to flex only when visible */
}

.notification-panel.show {
    display: flex !important;
    flex-direction: column;
}

.notification-panel-header {
    display:         flex;
    justify-content: space-between;
    align-items:     center;
    padding:         0.5rem 0.75rem;
    border-bottom:   1px solid var(--bs-border-color);
    background:      var(--bs-secondary-bg);
    flex-shrink:     0;
    font-size:       0.78rem;
    font-weight:     600;
    color:           var(--bs-secondary-color);
    text-transform:  uppercase;
    letter-spacing:  0.05em;
}

.notification-panel-body {
    flex: 1;
    overflow-y: auto;
    min-height: 0;
}

.notification-panel-footer {
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--bs-border-color);
    text-align: center;
    flex-shrink: 0;
    font-size: 0.8rem;
}

.notification-empty {
    padding: 2rem 1rem;
    text-align: center;
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
}

/* Auth buttons — on dark header */
.btn-header-login {
    background-color: #67B1E2;
    border-color: #67B1E2;
    color: #131022 !important;
    font-weight: 600;
    letter-spacing: 0.02em;
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.btn-header-login:hover,
.btn-header-login:focus {
    background-color: #90c8f0;
    border-color: #90c8f0;
    color: #131022 !important;
    box-shadow: 0 0 0 3px rgba(103, 177, 226, 0.3);
}

.btn-header-register {
    background-color: transparent;
    border-color: rgba(103, 177, 226, 0.4);
    color: #67B1E2 !important;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

.btn-header-register:hover,
.btn-header-register:focus {
    background-color: rgba(103, 177, 226, 0.1);
    border-color: #67B1E2;
    color: #90c8f0 !important;
}

/* btn-action — transition + disabled affordances only. Colours, hover/active, min-width and
   font-weight now live in the .btn-action variant block near the top (deduped 2026-06-13: the
   old duplicate here used `color:#131022 !important`, which had been overriding the disabled
   text colour — the long-standing unreadable-disabled-Save bug). */
.btn-action {
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.btn-action:disabled,
.btn-action[aria-disabled="true"] { cursor: not-allowed; }

/* Suppress any lingering focus ring from Bootstrap when the button is disabled.
   ‼️ :disabled ONLY — an aria-disabled button (disabled WITH a reason) is reachable by Tab on purpose, so
   its ring must show: a keyboard user has to see where the focus is (WCAG 2.4.7). */
.btn-action:disabled:is(:focus, :focus-visible) {
    box-shadow: none;
    outline: none;
}

/* Global button focus — WCAG 2.1 AA: solid outline for all buttons, consistent with other controls */
.btn:focus-visible {
    outline:        2px solid #67B1E2;
    outline-offset: 2px;
    box-shadow:     none;
}

/* ‼️ A TIER BUTTON DRAWS ITS FOCUS RING INSIDE ITSELF — the same picture on every screen (owner,
   2026-10-01, his 1.9 = A). A focused outline button reads as its own frame, a 2px gap, then the 2px
   ring: at `outline-offset: -5px` the ring sits 3–5px inside the edge. Inside, because OUTSIDE cannot be
   the same everywhere: a grid host is `overflow: hidden`, a window body and a tab panel are scroll
   containers with no margin, and every one of them clips at its own edge (`overflow-clip-margin` is
   ignored on a scroll container) — so a ring 2px outside the button was whole on Reference → Button and
   cut off on the edge of every list toolbar and window, which is why the DataGrid toolbar had to move it
   inside on its own and the two looked different. An outline never takes room: no button grows.
   An ICON-ONLY button keeps the ring at -2px: its glyph fills it, and a ring 5px in runs over the glyph.
   Same selector set as the shared minimum width above. Hand-built members of the family (AdminDB's
   .adb-save-btn, EntryPanel's .ep-btn--primary, ConfirmModal's buttons) carry the same offset in their
   own stylesheets. */
/* signal-colour: names Bootstrap's btn-outline-danger CLASS to give it the tier focus ring — a selector, not a colour */
.btn:is(.btn-action, .btn-secondary-action, .btn-utility, .btn-destructive, .btn-primary, .btn-warning-action, .btn-outline-danger, .btn-glyph):focus-visible {
    outline-offset: -5px;
}
/* signal-colour: the same selector set, for the icon-only ring */
.btn:is(.btn-action, .btn-secondary-action, .btn-utility, .btn-destructive, .btn-primary, .btn-warning-action, .btn-outline-danger, .btn-glyph):is(.btn-icon,:has(> :is(i, .bi, svg, img, .spinner-border):only-child), :has(> .visually-hidden)):focus-visible {
    outline-offset: -2px;
}

/* ── THE PORTAL'S (i) — one look for every "there is an explanation here" icon (owner, 2026-10-01) ──
   A bare info icon in the secondary text colour, no frame, that takes the text colour on hover and on
   keyboard focus; the explanation is the portal's own tooltip (tooltip.js). The element carries
   `tabindex="0"`, `role="note"` and the whole text as its `aria-label`. Worn by a DataGrid group row
   (.dg-cg-info) and the Timeline's legend (.tl-info) — each keeps only its own PLACEMENT in its own
   stylesheet. Before this class both carried a copy of these rules (owner, 1.8 = A: "Laat het (i)
   hetzelfde (i) zijn zoals bij een datagrid!"). Styling.md → The portal's (i). */
.portal-info-icon {
    display:        inline-flex;
    align-items:    center;
    color:          var(--bs-secondary-color);
    font-size:      0.82rem;
}
.portal-info-icon:hover { color: var(--bs-body-color); }
.portal-info-icon:focus-visible {
    color:          var(--bs-body-color);
    outline:        2px solid var(--signal-action-solid);
    outline-offset: 2px;
    border-radius:  0.2rem;
}

/* =============================================================
   App body — sidebar + main side by side
   ============================================================= */
#app-body {
    flex: 1;
    display: flex;
    overflow: hidden;
    min-height: 0;
}

/* =============================================================
   Sidebar
   ============================================================= */
#app-sidebar {
    flex-shrink: 0;
    /* --sidebar-user-width = the width the reader dragged the divider to (#sidebar-splitter below); the
       server writes it from the portal.sidebar.width cookie for the first paint. Unset → the default. */
    width: var(--sidebar-user-width, var(--sidebar-width));
    max-height: 100%;    /* anchor for internal flex column — ensures footer is never pushed out of view */
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background-color: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
    transition: width var(--sidebar-speed) ease;
    z-index: 1020;
}

#app-sidebar.sidebar-collapsed {
    width: var(--sidebar-collapsed-width);
}

/* ── The divider between the menu and the work (owner, 2026-10-15, CL-767 point 1.3 = A) ──────────────
   The shared Splitter (components/splitter.js, wired by sidebar.js) on #sidebar-splitter, WITHOUT its
   buttons: the bar and its three dots. While the menu is open the bar IS the edge, so the sidebar drops
   its own right border; the icon rail (a fixed 52px) and the phone overlay (< 768px) have no divider,
   and there the border stays. */
#sidebar-splitter { display: none; }
@media (min-width: 768px) {
    #app-sidebar:not(.sidebar-collapsed) { border-right: 0; }
    #app-sidebar:not(.sidebar-collapsed) + #sidebar-splitter { display: block; }
}
/* While the bar is dragged the width follows the pointer — the collapse animation would make it lag. */
#app-body:has(> #sidebar-splitter.is-dragging) > #app-sidebar { transition: none; }
/* QUIET AT REST — this divider only (owner, 2026-10-15, CL-767 point 1.5: "Hij is nu namelijk heel dominant
   aanwezig!"). At rest it wears the menu's own colour and the menu's old edge line, with the dots hidden, so it
   reads as the menu's edge; it shows itself — the shared splitter's blue bar and white dots — only under the
   pointer, while dragged, and on keyboard focus. Every other splitter in the portal keeps its visible bar. */
#sidebar-splitter.splitter {
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
}
#sidebar-splitter.splitter::after { opacity: 0; }
#sidebar-splitter.splitter:hover,
#sidebar-splitter.splitter.is-dragging { background: var(--sidebar-active-accent); }   /* = the splitter's own blue */
#sidebar-splitter.splitter:hover::after,
#sidebar-splitter.splitter.is-dragging::after,
#sidebar-splitter.splitter:focus-visible::after { opacity: 1; }

/* ============================================================================
   Global scrollbars — thin, rounded "floating pill", brand-blue on hover.
   Semi-transparent grey works on both light and dark themes. More specific
   rules (e.g. .sidebar-menu below) still win for their own areas.
   ============================================================================ */
:root {
    --scrollbar-thumb:       rgba(100, 110, 130, 0.45);
    --scrollbar-thumb-hover: var(--brand-primary, #67B1E2);
}
[data-bs-theme="dark"] {
    --scrollbar-thumb:       rgba(180, 190, 210, 0.35);
}

* {
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) transparent;
}
*::-webkit-scrollbar { width: 11px; height: 11px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
    background-color: var(--scrollbar-thumb);
    border: 3px solid transparent;          /* padding → slim floating pill */
    border-radius: 99px;
    background-clip: padding-box;
    transition: background-color 0.15s ease;
}
*::-webkit-scrollbar-thumb:hover,
*::-webkit-scrollbar-thumb:active {
    background-color: var(--scrollbar-thumb-hover);
    background-clip: padding-box;
}
*::-webkit-scrollbar-corner { background: transparent; }

/* Scrollable menu area */
.sidebar-menu {
    flex: 1;
    min-height: 0;       /* allows shrinking so the footer is never clipped */
    overflow-y: auto;
    overflow-x: hidden;
    padding: 0.375rem 0;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
}

.sidebar-menu::-webkit-scrollbar { width: 3px; }
.sidebar-menu::-webkit-scrollbar-track { background: transparent; }
.sidebar-menu::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, 0.1);
    border-radius: 2px;
}

.sidebar-nav {
    list-style: none;
    margin: 0;
    padding: 0;
}

.sidebar-item {
    margin: 1px 0;
}

/* Individual sidebar link */
.sidebar-link {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.55rem 0.75rem 0.55rem 0.85rem;
    margin: 0 0.5rem;                       /* inset so the rounded tile corners read correctly */
    color: var(--sidebar-color) !important;
    text-decoration: none !important;
    border-radius: 0.5rem;                  /* tile radius (data-grid .dg-card) */
    border: 1px solid transparent;          /* tile border — accent shows on hover/active */
    white-space: nowrap;
    overflow: hidden;
    position: relative;                     /* containing block for the glass (::before) and the polish (::after) */
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease;
}

/* ── Hover and the selected item wear the SAME polished glass as a card tile (owner, 2026-09-30: "laten we nu
   ook het menu dezelfde 'selected' weergeven geven … de niet geselecteerde item vooral zoals het is! alleen de
   selected en hover gelijk trekken"). An item at REST is untouched: no fill, no border, no light.
   Built from the tile's own tokens in data-grid.css (`--dg-card-navy`, `--dg-card-body`, `--dg-card-shade`,
   `--dg-card-rim-lit`), so the menu and the tiles cannot drift apart:
   - the GLASS (::before) — the navy glass over the black body; it fades in;
   - the POLISH (::after) — the TOP SLICE of the tile's bowed highlight, drawn at a tile's size;
   - the RIM — a gradient border (the border-box background behind the transparent 1px border): white to action
     blue under the pointer, the solid action blue on the selected item. The padding box of that background
     is painted in the sidebar's own colour, so the rim never shows through the glass while it fades in.
   The drop shadow keeps a negative spread: `.sidebar-menu` clips sideways, as a grid's scroll box does. */
.sidebar-link::before,
.sidebar-link::after {
    content: ""; position: absolute; inset: 0; border-radius: calc(0.5rem - 1px); pointer-events: none; z-index: 0;
    opacity: 0; transition: opacity 0.3s ease;
}
/* ‼️ FLAT, like the tile — not a bulging button (owner, 2026-09-30, OPEN-354 = A). The tile's recipe on a 44px
   item read as convex: measured top to bottom, the glass fell steadily from 0.053 to 0.030 (1.77×), where a
   122px tile stays flat (1.12×). Two things did it: the tile's inner shade has a FIXED size (14–22px), a fifth
   of a tile but half of a menu item; and the highlight and the light off the bottom edge are sized in
   PERCENTAGES, so on a low item they crowd together. So a menu item has no inner shade and no bottom light, and
   its highlight is drawn at a TILE's size (radius 76px, centre 24px above the top, as the tile's 62% / -20% of
   ~122px): the item shows only the top slice of the same bow, as the top of a tile does. */
.sidebar-link::before {
    background: var(--dg-card-navy), var(--dg-card-body);
}
.sidebar-link::after {
    background:
        radial-gradient(125% 76px at 28% -24px, color-mix(in srgb, white 20%, transparent),
                        color-mix(in srgb, white 5%, transparent) 58%, transparent 59%) no-repeat;
}
.sidebar-link:hover::before, .sidebar-link:hover::after,
.sidebar-link.active::before, .sidebar-link.active::after { opacity: 1; }

/* Keep the link content above the glass and the polish. */
.sidebar-link > * { position: relative; z-index: 1; }

/* Hover — the tile's hover: the navy glass fades in and the rim lights up (no lift). */
.sidebar-link:hover {
    background: linear-gradient(var(--sidebar-bg), var(--sidebar-bg)) padding-box, var(--dg-card-rim-lit) border-box;
    border-color: transparent;
    box-shadow: 0 14px 24px -14px color-mix(in srgb, black 90%, transparent);
    color: #e8eaf6 !important;
}

/* The icon colours of the menu are the COMMUNITY's (owner, 2026-10-16): community.js paints --sidebar-icon-color,
   --sidebar-icon-active and --sidebar-icon-hover on #app-sidebar for the active community (community-identity.js
   paintCommunityIcons). The values below are what stands while nothing is painted. */
.sidebar-link:hover .sidebar-icon {
    color: var(--sidebar-icon-hover, #90c8f0);
}

/* Selected — the tile's selected state: the navy glass standing, the rim in the solid action blue. After the
   hover rule and just as specific, so under the pointer it keeps its blue. */
.sidebar-link.active {
    background: linear-gradient(var(--sidebar-bg), var(--sidebar-bg)) padding-box,
                linear-gradient(var(--signal-action-solid), var(--signal-action-solid)) border-box;
    color: var(--sidebar-active-color) !important;
    border-color: transparent;
    box-shadow: 0 10px 26px -10px color-mix(in srgb, black 55%, transparent);
}

.sidebar-link.active .sidebar-icon {
    color: var(--sidebar-icon-active);
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sidebar-icon-active) 55%, transparent));
}

.sidebar-icon {
    font-size: 1.15rem;
    flex-shrink: 0;
    width: 1.3rem;
    text-align: center;
    color: var(--sidebar-icon-color);
    transition: color 0.15s ease, filter 0.15s ease;
}

/* Icon-less menu item: keep the icon column's width so the label aligns with icon'd items. */
.sidebar-icon-placeholder {
    display: inline-block;
}

.sidebar-text {
    font-size: 0.875rem;
    font-weight: 450;
    letter-spacing: 0.01em;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: opacity var(--sidebar-speed) ease, max-width var(--sidebar-speed) ease;
    max-width: 180px;
}
/* In the live menu the label takes all the room its row has: the menu's width is the reader's own (the
   divider above), and a fixed cap would still cut a label off in a menu dragged wide enough for it.
   A percentage, not `none`, so collapsing to the rail still animates the label away (→ 0). */
#app-sidebar .sidebar-text {
    max-width: 100%;
    min-width: 0;
}

/* Submenu group header — a parent WITHOUT a link of its own. It is typeset exactly like a menu item
   (same size, same case, same weight — no uppercase, no small caps; owner 2026-09-03) and is told apart
   ONLY by its 55 % fade: it opens nothing itself, a click merely expands or collapses its children. */
.sidebar-link-group {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.55rem 0.75rem 0.55rem 0.85rem;   /* same box as .sidebar-link so rows line up */
    margin: 0 0.5rem;                /* match .sidebar-link inset so the icon/text line up */
    color: var(--sidebar-color);
    cursor: pointer;                 /* a click-to-collapse toggle (MenuView) */
    -webkit-user-select: none;
    user-select: none;
    white-space: nowrap;
    overflow: hidden;
}
/* Fade only the label (icon + text) so the chevron toggle stays dominant. */
.sidebar-link-group .sidebar-icon,
.sidebar-link-group .sidebar-text { opacity: 0.55; transition: opacity 0.15s ease; }
.sidebar-link-group:hover .sidebar-icon,
.sidebar-link-group:hover .sidebar-text,
.sidebar-link-group:focus-visible .sidebar-icon,
.sidebar-link-group:focus-visible .sidebar-text { opacity: 0.9; }

/* A parent WITH a link (tool or URL) is an ordinary .sidebar-link — full strength, hover, active —
   with the expand/collapse on its own chevron button beside the tile (MenuView `.sidebar-parent-row`).
   The link gives up its right inset so the chevron sits where the group header's chevron sits. */
.sidebar-parent-row {
    display: flex;
    align-items: center;
}
.sidebar-parent-row > .sidebar-link {
    flex: 1 1 auto;
    min-width: 0;
    margin-right: 0;
}
.sidebar-group-toggle {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    margin-right: 0.5rem;
    padding: 0;
    border: 0;
    border-radius: 0.375rem;
    background: transparent;
    color: var(--sidebar-color);
    cursor: pointer;
}
.sidebar-group-toggle:hover,
.sidebar-group-toggle:focus-visible { background-color: var(--sidebar-hover-bg); }
.sidebar-group-toggle:focus-visible { outline: 2px solid #67B1E2; outline-offset: 1px; }

/* Expand/collapse a parent group (MenuView renders a chevron + toggles .collapsed on the <li>).
   The chevron is the dominant affordance: larger, bold, accent-coloured, full strength. */
.sidebar-item-has-children > .sidebar-link-group .submenu-chevron,
.sidebar-item-has-children > .sidebar-parent-row .submenu-chevron {
    margin-left: auto;
    font-size: 1rem;
    font-weight: 700;
    color: #67B1E2;
    opacity: 0.95;
    transition: transform 0.15s ease;
}
.sidebar-item-has-children.collapsed > .sidebar-link-group .submenu-chevron,
.sidebar-item-has-children.collapsed > .sidebar-parent-row .submenu-chevron { transform: rotate(-90deg); }
.sidebar-item-has-children.collapsed > .sidebar-submenu { display: none; }

/* Separators — a root separator spans from the front; a submenu separator is indented to its
   level so it lines up with the submenu items (.submenu-link padding-left). */
.sidebar-divider {
    border: 0;
    /* Theme-aware: a fraction of the sidebar text colour reads on both light and dark canvases
       (the old --sidebar-border was too dark to see). */
    border-top: 1px solid var(--sidebar-color);
    opacity: 0.3;
    margin: 6px 0.85rem;
}
/* Line up with the submenu items at its own depth — the same start as .submenu-link's padding-left. */
.sidebar-submenu .sidebar-divider { margin-left: calc(0.85rem + var(--menu-depth, 1) * 1.3rem); }

/* ── Rows supplied by a MENU SOURCE (a configured item whose children are resolved at render time) ──
   The heading such a row sits under is now an ordinary configured menu item, so it needs no styling of
   its own — that is the whole point of the source mechanism and why the former .sidebar-fav-heading is
   gone. What remains is the row itself: a source label can be a PATH ("Reference data /
   Core.TableMaster") rather than a menu word, so it needs more characters per line than a menu item
   does — smaller type + a tighter line, truncated with an ellipsis rather than wrapped (owner rule
   2026-07-31 — "ze passen zo nooit"). The row keeps the menu's height and hit area; only the text
   scales down. */
.sidebar-link[data-menu-source="1"] .sidebar-text {
    font-size:     0.78rem;
    line-height:   1.15;
    overflow:      hidden;
    text-overflow: ellipsis;
    white-space:   nowrap;
}

/* ── A LIVE badge (Core.Menuitem.BadgeSourceCode, menu-builder.md §3.9) ──────────────────────────────────────────────
   MenuView paints the number of a badge source after the label and sets its fill on the element: the priority's signal
   theme (--signal-{theme}-solid / -on-solid, its -border as a hairline so a light fill such as the Low-priority yellow
   still shows its edge on a light menu), else the community's colour with a readable text on it. Without either it
   wears the neutral Information fill below. It joins the row when its source answers and never makes the row taller:
   the pill is shorter than the icon line (measured in headless Chrome, MenuView.md §10). */
.sidebar-live-badge {
    background:           var(--live-badge-bg, var(--signal-info-solid));
    color:                var(--live-badge-fg, var(--signal-info-on-solid));
    box-shadow:           inset 0 0 0 1px var(--live-badge-ring, transparent);
    font-variant-numeric: tabular-nums;
    flex-shrink:          0;
}
/* The icon-only rail has no room beside the icon: the number sits on the tile's top-right corner, like the bell's. */
#app-sidebar.sidebar-collapsed .sidebar-live-badge {
    position:    absolute;
    top:         1px;
    right:       1px;
    margin:      0;
    padding:     0.15em 0.35em;
    font-size:   0.6rem;
    line-height: 1;
}
/* The MARK before a live badge (CL-807, an answer's `alert`): the one icon of its signal theme - the Warning triangle for
   Documents -> Processing when a text could not be read - in that theme's -fg, set by MenuView as --live-badge-alert-fg.
   The badge's own font size, so it never makes the row taller; on the icon-only rail it sits on the tile's top-left corner. */
.sidebar-live-badge-alert {
    display:     inline-flex;
    align-items: center;
    flex-shrink: 0;
    font-size:   0.75rem;
    line-height: 1;
    color:       var(--live-badge-alert-fg, var(--signal-warning-fg));
}
#app-sidebar.sidebar-collapsed .sidebar-live-badge-alert {
    position:  absolute;
    top:       1px;
    left:      1px;
    margin:    0;
    font-size: 0.6rem;
}

/* ── Menu consistency: account dropdown follows the main-menu line ───────────────
   Same label font as the sidebar (.sidebar-text = 0.875rem) and accent-blue icons. The menu
   builder's tree already reuses .sidebar-icon/.sidebar-text directly. */
.account-menu .dropdown-item { font-size: 0.875rem; }
.account-menu .dropdown-item > i,
.account-menu .dropdown-item > .me-2 { color: var(--sidebar-icon-color, #67B1E2); }
.account-menu .account-setting-label { font-size: 0.875rem; }

/* Submenu */
.sidebar-submenu {
    list-style: none;
    padding: 2px 0 4px 0;
    margin: 0;
}

.submenu-link {
    /* Depth-aware indent: every level steps in by exactly ONE ICON WIDTH (.sidebar-icon = 1.3rem) from
       the top level's 0.85rem, so a child's icon starts where its parent's icon ends (owner, 2026-10-15,
       CL-767 point 1.2: "Laten we het inspringen minder diep doen. Nu staat het 2de niveau meer dan 1
       icoon breedte ingesprongen"). It used to be 3.1rem for level 1 and 2rem per deeper level — 1.7×
       an icon at the first step, which spent a narrow menu's width on indentation instead of on labels.
       --menu-depth is set per <li> by MenuView (0 = top, 1 = first submenu, …); the fallback 1 keeps a
       lone .submenu-link correct. */
    padding-left: calc(0.85rem + var(--menu-depth, 1) * 1.3rem) !important;
    font-size: 0.82rem !important;
}
/* A nested parent group header (a submenu item that itself has children) indents like its level too —
   SAME formula as .submenu-link so a group header aligns with the leaf items at its own depth (a group at
   depth N sits on the same left edge as a leaf at depth N; its children step one level further). Using
   the 0.85rem top-level base alone put a nested group back at the far-left column (same x as a root
   group), so a 3rd-level item read as mis-indented until the group was flattened to a leaf. */
.sidebar-submenu .sidebar-item-has-children > .sidebar-link-group {
    padding-left: calc(0.85rem + var(--menu-depth, 1) * 1.3rem);
}

/* Collapsed state */
#app-sidebar.sidebar-collapsed .sidebar-text,
#app-sidebar.sidebar-collapsed .submenu-chevron {
    opacity: 0;
    max-width: 0;
    overflow: hidden;
    pointer-events: none;
}

#app-sidebar.sidebar-collapsed .sidebar-link {
    justify-content: center;
    width: 36px;             /* tight square selection tile, hugs the icon */
    height: 36px;
    padding: 0;              /* no padding — icon centers via flex */
    margin: 2px auto;        /* auto margins → guaranteed equal left/right gap */
    gap: 0;
}

/* Collapsed: the rail is narrow and #app-sidebar clips at its edge, so the 36px tile keeps its glass, polish
   and rim but drops the drop shadow — nothing reaches past it. */
#app-sidebar.sidebar-collapsed .sidebar-link:hover,
#app-sidebar.sidebar-collapsed .sidebar-link.active {
    box-shadow: none;
}

#app-sidebar.sidebar-collapsed .sidebar-item-has-children > .sidebar-link-group,
#app-sidebar.sidebar-collapsed .sidebar-group-toggle {
    display: none;
}
/* In the icon-only sidebar a linked parent keeps its icon tile like any other item. */
#app-sidebar.sidebar-collapsed .sidebar-parent-row > .sidebar-link { margin-right: 0.5rem; }

#app-sidebar.sidebar-collapsed .sidebar-submenu {
    padding-left: 0;
}
/* The rail has no group header and no chevron to open a group by (both hidden above), so it shows every group's items
   whether the group is open or not: since every group starts collapsed (CL-809, MenuView.md §5), a collapsed group
   would otherwise take its items out of the rail with no way to reach them. Open/closed applies to the wide menu. */
#app-sidebar.sidebar-collapsed .sidebar-item-has-children.collapsed > .sidebar-submenu {
    display: block;
}

#app-sidebar.sidebar-collapsed .submenu-link {
    width: 36px;                     /* match the collapsed top-level tile */
    height: 36px;
    padding: 0 !important;
    margin: 2px auto;                /* auto margins → guaranteed equal left/right gap */
    justify-content: center;
}

/* Sidebar footer */
.sidebar-footer {
    flex-shrink: 0;
    border-top: 1px solid var(--sidebar-border);
    padding: 0.5rem 0.75rem 0.5rem 0.85rem;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

/* Brand link: icon + wordmark side by side */
.sidebar-brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none !important;
    overflow: hidden;
    flex-shrink: 0;
}

.sidebar-brand:hover { opacity: 0.8; }

.sidebar-brand-icon {
    flex-shrink: 0;
    width: 1.3rem;
    height: auto;
}

/* Wordmark: light/dark theme-aware */
.sidebar-brand-wordmark {
    height: 16px;
    width: auto;
    flex-shrink: 0;
    transition: opacity var(--sidebar-speed) ease, max-width var(--sidebar-speed) ease;
    max-width: 120px;
    overflow: hidden;
}

.sidebar-brand-wordmark-dark  { display: none; }
.sidebar-brand-wordmark-light { display: block; }

[data-bs-theme="dark"] .sidebar-brand-wordmark-light { display: none; }
[data-bs-theme="dark"] .sidebar-brand-wordmark-dark  { display: block; }

/* Meta: version + session */
.sidebar-footer-meta {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transition: opacity var(--sidebar-speed) ease, max-height var(--sidebar-speed) ease;
    max-height: 2rem;
}

.sidebar-version {
    font-size: 0.63rem;
    color: var(--sidebar-color);
    opacity: 0.65;
    white-space: nowrap;
}

.sidebar-session {
    font-size: 0.62rem;
    color: var(--sidebar-color);
    opacity: 0.55;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: default;
}

/* Collapsed state: hide wordmark and meta */
#app-sidebar.sidebar-collapsed .sidebar-brand-wordmark {
    opacity: 0;
    max-width: 0;
    overflow: hidden;
    pointer-events: none;
}

#app-sidebar.sidebar-collapsed .sidebar-footer-meta {
    opacity: 0;
    max-height: 0;
    overflow: hidden;
    pointer-events: none;
}

#app-sidebar.sidebar-collapsed .sidebar-brand {
    justify-content: center;
    gap: 0;
}

/* =============================================================
   Main area
   ============================================================= */
#app-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-width: 0;
}

/* Server-side notification alerts (below header) */
#notification-area {
    flex-shrink: 0;
}

.portal-alert {
    display: flex;
    align-items: center;
    padding: 0.55rem var(--portal-gutter-end) 0.55rem var(--portal-gutter);   /* the portal gutter */
    margin: 0;
    border: none;
    border-radius: 0;
    font-size: 0.855rem;
    border-bottom: 1px solid transparent;
}

.portal-alert-success {
    background-color: #dcfce7;
    color: #166534;
    border-bottom-color: #bbf7d0;
}

.portal-alert-error {
    background-color: #fee2e2;
    color: #991b1b;
    border-bottom-color: #fecaca;
}

.portal-alert-info {
    background-color: #dbeafe;
    color: #1e40af;
    border-bottom-color: #bfdbfe;
}

[data-bs-theme="dark"] .portal-alert-success {
    background-color: #052e16;
    color: #86efac;
    border-bottom-color: #14532d;
}

[data-bs-theme="dark"] .portal-alert-error {
    background-color: #2d0b0b;
    color: #fca5a5;
    border-bottom-color: #7f1d1d;
}

[data-bs-theme="dark"] .portal-alert-info {
    background-color: #0c1a4e;
    color: #93c5fd;
    border-bottom-color: #1e3a8a;
}

/* Empty-tool placeholder — shown when a menu item has no URL and no ToolEntrypoint */
.tool-empty-title {
    font-size: 1.25rem;
    font-weight: 600;
    margin: 0;
    color: var(--bs-body-color);
}

/* =============================================================
   Portal breadcrumb bar
   Sits between the notification area and the content area.
   Revealed / populated by tools via /js/modules/breadcrumb.js.
   ============================================================= */
:root {
    --bc-bg:      var(--portal-chrome-bg);   /* the same surface as the tab strip and the help line */
    --bc-border:  rgba(0, 0, 0, 0.10);       /* a contrast inside the chrome band, never a fixed grey */
    --bc-link:    #202549;
    --bc-current: #3c384f;
}

[data-bs-theme="dark"] {
    --bc-bg:      var(--portal-chrome-bg);   /* the same surface as the tab strip and the help line */
    --bc-border:  rgba(255, 255, 255, 0.10);   /* a contrast inside the chrome band, never a fixed grey */
    --bc-link:    #90c8f0;
    --bc-current: #dddaf0;
}

#portal-breadcrumb {
    flex-shrink:      0;
    display:          flex;
    align-items:      center;
    min-height:       36px;
    padding:          0 var(--portal-gutter-end) 0 var(--portal-gutter);   /* the portal gutter */
    background-color: var(--bc-bg);
    border-bottom:    1px solid var(--bc-border);
    overflow:         hidden;
}

.portal-bc-list {
    display:     flex;
    align-items: center;
    flex-wrap:   wrap;
    gap:         0;
    list-style:  none;
    margin:      0;
    padding:     0;
    min-width:   0;
}

.portal-bc-item {
    display:    flex;
    align-items: center;
    min-width:   0;
    font-size:   0.8rem;
    white-space: nowrap;
}

.portal-bc-sep {
    font-size:   0.6rem;
    color:       var(--bs-secondary-color, #6c757d);
    opacity:     0.7;
    margin:      0 0.3rem;
    flex-shrink: 0;
}

.portal-bc-icon {
    font-size:    0.8rem;
    margin-right: 0.3rem;
    opacity:      0.8;
    flex-shrink:  0;
}

.portal-bc-link {
    display:       inline-flex;
    align-items:   center;
    color:         var(--bc-link);
    text-decoration: none;
    font-weight:   500;
    border-radius: 4px;
    padding:       0.15rem 0.3rem;
    max-width:     18rem;
    overflow:      hidden;
    text-overflow: ellipsis;
    transition:    color 0.15s ease, background-color 0.15s ease;
}

.portal-bc-link:hover {
    color:            var(--bs-primary, #E50B46);
    background-color: rgba(229, 11, 70, 0.06);
    text-decoration:  none;
}

.portal-bc-link:focus-visible {
    outline:        2px solid #67B1E2;
    outline-offset: 2px;
}

/* An item that runs a click instead of following a link (breadcrumb.js `onClick`: the Excel task pane's own views). */
button.portal-bc-button {
    background: none;
    border:     0;
    font:       inherit;
    font-weight: 500;
    cursor:     pointer;
}

.portal-bc-current {
    display:       inline-flex;
    align-items:   center;
    color:         var(--bc-current);
    font-weight:   500;
    padding:       0.15rem 0.3rem;
    max-width:     22rem;
    overflow:      hidden;
    text-overflow: ellipsis;
}

@media (max-width: 575.98px) {
    #portal-breadcrumb {
        min-height: 32px;
    }

    .portal-bc-link    { max-width: 10rem; }
    .portal-bc-current { max-width: 14rem; }
}

/* ── THE PORTAL GUTTER — one left edge and one right edge for every screen (owner, 2026-09-13) ─────
   ‼️ WHY THE SHELL OWNS IT, AND NO SCREEN DOES. Until 2026-09-13 `#app-content` dropped its padding to 0
   the moment a screen mounted a ToolPage (tool-page.css), and every screen then invented its own edge:
   AdminDB 1.25rem through a private token, the page bar 1rem, Explore 1.25rem on top of the shell's
   1.5rem, and the Home dashboard, the value lists and half the Maintenance screens nothing at all. The
   owner saw the result on three screens in one sitting: tiles flush against the menu, an [Edit] that
   did not line up with the tiles under it, a tab line further left than the sentence above it.
   OPEN-173 had chosen a class every screen would adopt "when it is next touched" (`.portal-screen`);
   one screen had adopted it in a week. So the class is gone, and the number lives here, on the shell.

   The three tokens, and who reads them:
     --portal-gutter       the inset from the menu edge. `#app-content` pads by it in EVERY mode (plain,
                           tool-mode, tool-fill), and so do the page bar, the breadcrumb, the maintenance
                           band, the screen's lead line and the (?) panel, which live in the shell.
     --portal-scrollbar-w  the width `#app-content` reserves for its scrollbar (`scrollbar-gutter:
                           stable`). Measured by shell-gutter.js — 0 with overlay scrollbars, ~15px with
                           classic ones — because CSS cannot read it.
     --portal-gutter-end   the RIGHT inset for anything OUTSIDE `#app-content`: gutter + that scrollbar.
                           Without it a page-bar button stands a scrollbar-width to the right of the
                           tiles under it, which is exactly the misalignment this exists to remove.

   ‼️ A SCREEN NEVER PADS ITS OWN ROOT HORIZONTALLY AGAIN. Everything inside `#app-content` already
   starts on the gutter; a root padding doubles it. Enforced by ScreenGutterContractTests in
   Portal/Web.Tests, which refuses a rule that zeroes `#app-content`'s horizontal padding and a private
   inset token. Vertical spacing stays the screen's own business. */
/* ‼️ HALVED ON 2026-09-13 (owner, straight after seeing it on screen): 1.25rem → 0.625rem, and the two
   compacted values with it, so the relationship between them is unchanged. The number the tools used to
   carry privately was chosen per tool; once one edge runs through the whole portal it reads as a wide
   empty band, and screen space is what a grid wants. Change it HERE and every screen follows. */
/* ‼️ ONE SURFACE FOR THE CHROME ABOVE A SCREEN (owner, 2026-09-13 22:30, as an experiment: *"laten we
   onze breadcrumb dezelfde achtergrond geven als de tabbar"* and the same for the Help panel). The tab
   strip, the breadcrumb and the screen's instruction line are three different components that stack
   into one band, so the colour lives here and each of them reads it — a value copied into three files
   is three values the day one of them is touched. Dark mode gets a translucent white rather than the
   light theme's grey, so the band lifts off the page instead of sitting as a slab on it.
   ‼️ Since 2026-10-01 the instruction line no longer wears this colour: it has its own (--note-help-bg,
   below), so the band is the breadcrumb and the tab strip. */
:root {
    --portal-chrome-bg:   var(--bs-tertiary-bg, #f4f5f7);
    /* ‼️ THE HELP NOTE LEFT THE BAND AGAIN (owner, 2026-10-01 15:15: *"in light mode moet die 'wit' worden.
       Hetzelfde als de standaard achtergrond!"*; dark mode wears the active tab's surface, below). It wore the chrome grey
       from 2026-09-13 22:30 until then. Light mode: the page's own background, so the line reads as part of
       the screen instead of as a grey strip. Its own knob, documented in note.css: set it on an ancestor and
       every Help note follows, without a call site or that component changing. The (?) panel under the line
       reads the same knob (module-help.css), so the line and its explanation stay one surface. */
    --note-help-bg:       var(--content-bg);
    --portal-gutter:      0.625rem;
    --portal-scrollbar-w: 0px;
    --portal-gutter-end:  calc(var(--portal-gutter) + var(--portal-scrollbar-w));
}

/* Dark mode: one step lighter than the page, so the band lifts off it instead of sitting on it as a slab.
   ‼️ A SOLID value, not a translucent white. The strips of this band do not all stand on the same ground —
   the page bar and the help line sit on the shell, the tab strip inside #app-content — so a translucent
   colour renders as two slightly different greys the moment those grounds differ, which is exactly the
   defect this token was introduced to remove. #2c2c2c is what 5% white over the dark page (#212121)
   computes to, so the look is unchanged and the value no longer depends on what is behind it. */
[data-bs-theme="dark"] {
    --portal-chrome-bg: #2c2c2c;
    /* Dark mode: THE ACTIVE TAB'S SURFACE (owner, 2026-10-01 15:24: *"in de dark mode, gebruik eens dezelfde
       achtergrond kleur zoals die ook gebruikt wordt bij een actieve tab op tabbar"*), NOT the page's own
       dark grey. It was black for ten minutes first (15:11, *"een zwarte achtergrond kleur"*). The same
       TOKEN the active tab reads (`.tb-btn[aria-selected="true"]` in tab-bar.css), not a copy of its value,
       so the two cannot drift apart; shell-bar-order.test.mjs compares them. A token, not a literal: a
       surface, not a colour that means something, so nothing new for the signal-theme ratchet to count. */
    --note-help-bg:     var(--bs-body-bg);
}

/* Phone: the page bar and the breadcrumb used to come in to .75rem here; now everything does, halved. */
@media (max-width: 575.98px) {
    :root { --portal-gutter: 0.375rem; }
}

/* Mobile landscape: the compaction AdminDB used to apply to its private token, now for every screen. */
@media (orientation: landscape) and (max-height: 520px) {
    :root { --portal-gutter: 0.4375rem; }
}

/* Content area — THE ONLY element that scrolls */
#app-content {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    /* Vertical 1.5rem for a plain page; horizontal is THE gutter (see above), never a number of its own.
       The right side needs no scrollbar term: this element's own scrollbar gutter lies outside its
       padding, so its content already ends where --portal-gutter-end ends for the shell bars. */
    padding: 1.5rem var(--portal-gutter);
    /* Reserve scrollbar gutter on the end side so content width never changes
       when a scrollbar appears or disappears (tab switching, overflow toggling). */
    scrollbar-gutter: stable;
    background-color: var(--content-bg);
    color: var(--content-color);
}

/* HTMX global loading indicator — thin progress bar at the top of the content area */
#htmx-spinner {
    position:   fixed;
    top:        0;
    left:       0;
    right:      0;
    height:     3px;
    background: #67B1E2;
    z-index:    9999;
    opacity:    0;
    transition: opacity 0.15s ease;
    pointer-events: none;
}
#htmx-spinner.htmx-request {
    opacity: 1;
    /* Indeterminate slide animation */
    animation: htmx-slide 1.2s ease-in-out infinite;
}
@keyframes htmx-slide {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(100%); }
}

/* =============================================================
   Mobile — sidebar as overlay
   ============================================================= */
@media (max-width: 767.98px) {
    #app-sidebar {
        position: absolute;
        left: 0;
        top: var(--header-height);
        height: calc(100dvh - var(--header-height));
        transform: translateX(-100%);
        transition: transform var(--sidebar-speed) ease;
        z-index: 1025;
        width: var(--sidebar-width) !important;
    }

    #app-sidebar.sidebar-open-mobile {
        transform: translateX(0);
    }
}

/* Mobile overlay */
#sidebar-mobile-overlay {
    display: none;
    position: fixed;
    inset: 0;
    top: var(--header-height);
    background: rgba(0, 0, 0, 0.45);
    z-index: 1024;
}

#sidebar-mobile-overlay.active {
    display: block;
}

@media (min-width: 768px) {
    #sidebar-mobile-overlay { display: none !important; }
}

/* =============================================================
   Utility
   ============================================================= */
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Square proportions for icon-only btn-sm buttons — overrides Bootstrap's text-optimised horizontal padding */
    padding-inline: 0.35rem;
}

/* Dropdown menus spawned from header sit flush */
#app-header .dropdown-menu {
    margin-top: 0.3rem;
}

/* =============================================================
   Registration modal — password strength
   ============================================================= */
.pwd-strength-track {
    height: 4px;
    border-radius: 2px;
    background-color: var(--bs-border-color);
}

.pwd-strength-track .progress-bar {
    border-radius: 2px;
    transition: width 0.3s ease, background-color 0.3s ease;
}

/* =============================================================
   Language loading state
   While the API auto-translates a new language (DeepL call),
   html.lang-loading is set. Entire page gets a wait cursor and
   the flag in the language button is replaced by a small spinner.
   ============================================================= */
html.lang-loading,
html.lang-loading * {
    cursor: wait !important;
}

html.lang-loading #lang-current-flag {
    display: inline-block;
    width:  1rem;
    height: 1rem;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: lang-spin 0.65s linear infinite;
    vertical-align: middle;
    font-size: 0 !important;   /* hide emoji text while spinning */
}

@keyframes lang-spin {
    to { transform: rotate(360deg); }
}

/* =============================================================
   Offline
   network.js sets body.app-offline while the connection is lost: the content is dimmed and
   blocked, so nothing is entered that cannot be saved. WHY it is dimmed is told by the red "!"
   on the header's network button (.support-exclaim below) — there is deliberately no bar over
   the screen any more (owner, 2026-10-09: it pushed the whole page down).
   ============================================================= */

body.app-offline #app-content {
    pointer-events: none;
    user-select:    none;
    opacity:        0.45;
    transition:     opacity 0.3s;
}

/* ── Support / network quality panel ──────────────────────────────────────── */

.support-panel {
    position:   fixed;
    top:        3.5rem;
    right:      0.75rem;
    width:      500px;
    max-width:  calc(100vw - 1.5rem);
    max-height: calc(100vh - 4.5rem);
    overflow:   hidden;
    z-index:    1080;
    display:    flex;
    flex-direction: column;
    background: var(--bs-body-bg);
    border:     1px solid var(--bs-border-color);
    border-radius: 0.5rem;
    box-shadow: 0 8px 32px rgba(0,0,0,.25);
    color:      var(--bs-body-color);
}
.support-panel[hidden] { display: none; }

/* ── Beside the page: the page's own edge overlays move in with the page (see "BESIDE the page" under #layout-wrapper) ──
   The header's dropdown panels and the support panel stand under their buttons again, the toasts in the page's corner, the
   slide-in EntryPanel and its backdrop at the page's edge, and the loading bar over the page — none of them over the chat. */
html.assistant-beside .notification-panel {
    top:   calc(3.5rem + var(--assistant-beside-top, 0px)) !important;
    right: calc(0.75rem + var(--assistant-beside-right, 0px)) !important;
}
html.assistant-beside .support-panel {
    top:   calc(3.5rem + var(--assistant-beside-top, 0px));
    right: calc(0.75rem + var(--assistant-beside-right, 0px));
}
html.assistant-beside #toast-container {
    right:  var(--assistant-beside-right, 0px) !important;
    bottom: var(--assistant-beside-bottom, 0px) !important;
}
html.assistant-beside .ep-panel {
    right:  var(--assistant-beside-right, 0px);
    top:    calc(var(--header-height, 56px) + var(--assistant-beside-top, 0px));
    height: calc(100dvh - var(--header-height, 56px) - var(--assistant-beside-top, 0px) - var(--assistant-beside-bottom, 0px));
}
html.assistant-beside .ep-backdrop {
    top:    calc(var(--header-height, 56px) + var(--assistant-beside-top, 0px));
    right:  var(--assistant-beside-right, 0px);
    bottom: var(--assistant-beside-bottom, 0px);
    left:   var(--assistant-beside-left, 0px);
}
html.assistant-beside #htmx-spinner {
    top:   var(--assistant-beside-top, 0px);
    right: var(--assistant-beside-right, 0px);
    left:  var(--assistant-beside-left, 0px);
}

@media (max-width: 520px) {
    .support-panel {
        left:   0.5rem;
        right:  0.5rem;
        width:  auto;
        top:    3.25rem;
    }
}

.sp-panel-header {
    display:         flex;
    justify-content: space-between;
    align-items:     center;
    padding:         0.5rem 0.75rem;
    border-bottom:   1px solid var(--bs-border-color);
    background:      var(--bs-secondary-bg);
    flex-shrink:     0;
}

.sp-panel-title {
    font-size:      0.78rem;
    font-weight:    600;
    color:          var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.sp-header-actions {
    display:     flex;
    align-items: center;
    gap:         0.25rem;
}

.sp-action-btn {
    display:       flex;
    align-items:   center;
    gap:           0.3rem;
    background:    none;
    border:        none;
    color:         var(--bs-secondary-color);
    font-size:     0.75rem;
    cursor:        pointer;
    padding:       0.25rem 0.4rem;
    border-radius: 4px;
    transition:    color 0.15s, background-color 0.15s;
    line-height:   1;
}
.sp-action-btn:hover         { color: var(--bs-body-color); background: var(--bs-tertiary-bg, rgba(0,0,0,.06)); }
.sp-action-btn:focus-visible { outline: 2px solid #67B1E2; outline-offset: 1px; }
.sp-action-btn:disabled      { opacity: 0.45; cursor: not-allowed; }

.sp-panel-body {
    overflow-y: auto;
    flex:       1;
    padding:    0;
}

.sp-status-row {
    display:       flex;
    align-items:   center;
    gap:           0.75rem;
    padding:       0.6rem 0.75rem;
    border-bottom: 1px solid var(--bs-border-color);
    flex-wrap:     wrap;
}

.sp-quality-badge {
    display:     flex;
    align-items: center;
    gap:         0.35rem;
}

.sp-quality-label { font-weight: 600; font-size: 0.82rem; }

.sp-stats {
    display:     flex;
    align-items: center;
    gap:         0.5rem;
    flex-wrap:   wrap;
    font-size:   0.8rem;
}

.sp-stat {
    display:     flex;
    align-items: baseline;
    gap:         0.3rem;
}

.sp-stat-lbl { color: var(--bs-secondary-color); }
.sp-stat-val { font-variant-numeric: tabular-nums; font-weight: 500; }

.sp-stat-sep {
    width:      1px;
    height:     0.85rem;
    background: var(--bs-border-color);
}

.sp-last-time {
    font-size:   0.72rem;
    color:       var(--bs-secondary-color);
    margin-left: auto;
}

.sp-chart-section {
    padding:       0.5rem 0.75rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.sp-chart-section:last-of-type { border-bottom: none; }

.sp-chart-label {
    font-size:     0.72rem;
    color:         var(--bs-secondary-color);
    margin-bottom: 0.3rem;
}

.sp-chart-wrap { height: 108px; }

.sp-chart-svg {
    display:       block;
    width:         100%;
    border-radius: 3px;
}

.sp-chart-svg .sp-chart-text { fill: var(--bs-secondary-color); font-size: 9px; font-family: inherit; }
.sp-chart-svg .sp-chart-grid { stroke: var(--bs-border-color); stroke-width: 1; fill: none; }

/* Real-time connections (support-panel.js, owner 2026-10-16, 1.2 = A): one row per SignalR socket — WEB and
   API — with its state in words, and a timeline of both. The rows share one grid so the states line up;
   a row is `display: contents`, its data-state colours the dot. On the timeline a connected stretch is a
   thin bar and a stretch without the connection a full-height block, so the history has a second channel
   beside the colour. Every colour is a signal theme: success = connected, warning = the page's own start-up,
   danger = the connection is lost. */
.sp-rt-rows {
    display:               grid;
    grid-template-columns: auto auto 1fr auto;
    align-items:           center;
    gap:                   0.2rem 0.45rem;
    font-size:             0.8rem;
    margin-bottom:         0.35rem;
}
.sp-rt-row   { display: contents; }
.sp-rt-name  { font-weight: 600; }
.sp-rt-dot   { width: 8px; height: 8px; border-radius: 50%; background: var(--signal-success-solid); }
.sp-rt-row[data-state="connecting"]   .sp-rt-dot { background: var(--signal-warning-solid); }
.sp-rt-row[data-state="reconnecting"] .sp-rt-dot,
.sp-rt-row[data-state="disconnected"] .sp-rt-dot { background: var(--signal-danger-solid); }
.sp-rt-count { font-size: 0.72rem; color: var(--bs-secondary-color); font-variant-numeric: tabular-nums; }

.sp-rt-chart { height: 52px; }
.sp-rt-svg .sp-rt-lane         { font-weight: 600; }
.sp-rt-svg .sp-rt-seg--connected    { fill: var(--signal-success-solid); }
.sp-rt-svg .sp-rt-seg--connecting   { fill: var(--signal-warning-solid); }
.sp-rt-svg .sp-rt-seg--reconnecting,
.sp-rt-svg .sp-rt-seg--disconnected { fill: var(--signal-danger-solid); }

@keyframes sp-spin { to { transform: rotate(360deg) } }
.sp-spin { display: inline-block; animation: sp-spin 0.9s linear infinite; }

/* Small spinner overlay — hangs off top-right corner of the support button */
.sp-measure-spinner {
    position:       absolute;
    top:            14px;
    right:          4px;
    transform:      translate(50%, -50%);
    line-height:    1;
    color:          #67B1E2;
    pointer-events: none;
    display:        inline-flex;
}
/* The rotation lives on the inner icon: an animation on `transform` replaces the span's positioning
   translate instead of adding to it, which made the icon slide to the centre rather than spin.
   The icon is drawn as a MASK of the Bootstrap Icons arrow-clockwise SVG on an exact 1em square,
   not as the icon-font glyph: a glyph sits on a baseline snapped to whole pixels, so its ring is
   never exactly on the rotation origin and it wobbles. The SVG ring is centred in its viewBox, so
   the square's centre IS the ring's centre. */
.sp-measure-spinner > i {
    display:          block;
    width:            1em;
    height:           1em;
    background-color: currentColor;
    -webkit-mask:     url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z'/%3E%3Cpath d='M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
    mask:             url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z'/%3E%3Cpath d='M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
    animation:        sp-spin 0.75s linear infinite;
}
.sp-measure-spinner > i::before { display: none; }
.sp-measure-spinner[hidden] { display: none; }

/* The connection warning ON the support button (support-panel.js), replacing the full-width bars
   that used to cover the header: a small disc with an "!" in the middle of the network icon —
   yellow at quality `warn`, red at `degraded`/`poor` and while offline. A DISC, not a bare
   glyph: a yellow "!" on the light theme's grey header is barely 1.1:1, while the disc carries
   its own background, so the "!" on it is readable on any header. The ring in the header colour
   lifts it off the bars behind it. Absolutely positioned, so showing it never moves anything. */
#support-btn { position: relative; }

.support-exclaim {
    position:       absolute;
    left:           50%;
    top:            50%;
    transform:      translate(-50%, -50%);
    width:          0.8rem;
    height:         0.8rem;
    border-radius:  50%;
    display:        flex;
    align-items:    center;
    justify-content: center;
    font-size:      0.7rem;
    line-height:    1;
    box-shadow:     0 0 0 1.5px var(--header-bg);
    pointer-events: none;
}
.support-exclaim[hidden] { display: none; }
.support-exclaim > i::before { font-weight: 700 !important; vertical-align: 0; }
.support-exclaim[data-level="warning"] {
    background-color: var(--signal-warning-solid);
    color:            var(--signal-warning-on-solid);
}
.support-exclaim[data-level="danger"] {
    background-color: var(--signal-danger-solid);
    color:            var(--signal-danger-on-solid);
}

.sp-browser-hint {
    padding:       0.4rem 0.75rem;
    font-size:     0.72rem;
    color:         var(--bs-secondary-color);
    font-style:    italic;
    border-bottom: 1px solid var(--bs-border-color);
}

.sp-settings {
    padding:    0.6rem 0.75rem;
    border-top: 1px solid var(--bs-border-color);
}

.sp-settings-title {
    font-size:      0.72rem;
    font-weight:    600;
    color:          var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom:  0.5rem;
}

.sp-settings-grid {
    display:               grid;
    grid-template-columns: 1fr auto;
    align-items:           center;
    gap:                   0.35rem 0.6rem;
    font-size:             0.8rem;
}

.sp-setting-label { color: var(--bs-body-color); }

.sp-setting-input-row {
    display:     flex;
    align-items: center;
    gap:         0.3rem;
}

.sp-setting-input {
    width:         4rem;
    padding:       0.2rem 0.35rem;
    font-size:     0.8rem;
    background:    var(--bs-body-bg);
    color:         var(--bs-body-color);
    border:        1px solid var(--bs-border-color);
    border-radius: 4px;
    text-align:    right;
}
.sp-setting-input:focus { outline: 2px solid #67B1E2; outline-offset: 1px; }

.sp-setting-unit { color: var(--bs-secondary-color); font-size: 0.75rem; }

/* ── The info marker ───────────────────────────────────────────────────────────
   The small muted (i) that sits behind a value and opens a tooltip explaining it.
   `<i class="bi bi-info-circle portal-info-icon" data-tip="…">`.

   Shared because it existed twice with the same two rules and a different name —
   `cfg-key-info` in Config (the security-key wizard) and `trls-info` in the RLS test
   tool — and the second copy was noticed only because it did NOT match: it had been
   drawn as a bordered circle with a literal "i" in it, and the owner spotted it on
   its first day. A marker that means "there is an explanation here" has to look the
   same everywhere, or the reader has to learn it twice.

   ‼️ NOT the same thing as `.exp-hint-info` (AdminDB) or `.ep-info-rule-icon`
   (EntryPanel), which also carry "info" in their names: the first is hint TEXT, the
   second a validation-status icon with its own valid/invalid/pending colours. They
   are deliberately left alone — merging on a shared substring rather than a shared
   meaning is how a utility class ends up owning three unrelated things.

   No pointer cursor: it explains, it does not navigate. Focusable at the call site
   (`tabindex="0"`) so the tooltip, which opens on focus too, is reachable without a
   mouse.                                                                          */
.portal-info-icon {
    margin-left: 0.35rem;
    color:       var(--bs-secondary-color);
}

.portal-info-icon:hover,
.portal-info-icon:focus-visible {
    color:       var(--bs-body-color);
}

/* Dark mode: an UNCHECKED checkbox or radio sits on the portal's field surface (--portal-field-*, defined with the field
   rule in components/entry-panel.css), the same fill and border as a text field or a dropdown, everywhere in the portal
   (owner, 2026-10-07, CL-709: generic across the whole portal). A checked one keeps Bootstrap's own fill. */
[data-bs-theme="dark"] .form-check-input:not(:checked):not(:indeterminate) {
    background-color: var(--portal-field-bg);
    border-color:     var(--portal-field-border);
}

/* Dark mode: EVERY control a reader can change sits on the field surface, wherever it stands — not only inside a form
   field (.ep-field, entry-panel.css) but in a toolbar, a band, a panel head, a dialog of its own. The fill says "you can
   change this" (owner, 2026-10-11, CL-731: "alle invoervelden die achtergrond geven, zodat een gebruiker altijd ziet wat
   hij kan wijzigen"). A disabled or read-only control keeps its own fill, so the difference stays visible.
   ‼️ background-COLOR, never the shorthand: a .form-select draws its caret as a background image.
   ‼️ Low specificity on purpose (:where): a component that styles its trigger inside a container of its own (FkSelect's
   picker, a field box) keeps its own rule, and Bootstrap's :focus fill loses to this one only by stylesheet order.
   A dropdown is disabled by `inert` on its ROOT (DropdownSelect, MultiSelect, FkSelect), so a trigger inside an inert
   element is excluded too — inside a :where, so the exclusion adds no specificity.
   The SegmentedControl track takes the same surface in its own stylesheet (segmented-control.css). */
:where([data-bs-theme="dark"]) :where(.form-control, .form-select, .ds-trigger, .ms-trigger, .sdd-trigger, .sb-input, .ai-field):not(:disabled):not([readonly]):where(:not([inert] *, [inert], [aria-disabled="true"])) {
    background-color: var(--portal-field-bg);
    border-color:     var(--portal-field-border);
    color:            var(--portal-field-fg);
}
