/* Go-tier grid corrections.
 *
 * Loaded AFTER the PHP stylesheets so it can override them. Everything here
 * fixes a measured defect in the Go pages; none of it restyles Chronos.
 */

/* ---------------------------------------------------------------------------
 * FILTER CONTROL HEIGHTS
 *
 * The filter row mixed three heights: a plain .form-control-sm at 31px, a
 * single TomSelect at 33px, and a multi TomSelect carrying a chip at 29px.
 * Because the row is bottom-aligned, unequal heights pushed the LABELS apart
 * too — measured at a 4px stagger across one row, which reads as a wobbly form
 * rather than as a sizing bug.
 *
 * TomSelect renders its own control and does not inherit Bootstrap's -sm
 * sizing, so the height has to be stated.
 * ------------------------------------------------------------------------ */
.modern-filter-form .ts-wrapper .ts-control,
.modern-filter-form .form-control-sm,
.modern-filter-form .form-select-sm {
    min-height: 31px;
    padding-top: 0.15rem;
    padding-bottom: 0.15rem;
    font-size: 0.875rem;
    line-height: 1.5;
}

/* A chip must not grow the control past the row's height — it scrolls
 * horizontally instead, which keeps every field on the same baseline however
 * many values are selected. */
.modern-filter-form .ts-wrapper.multi .ts-control {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
}

.modern-filter-form .ts-wrapper.multi .ts-control > .item {
    flex: 0 0 auto;
}

/* ---------------------------------------------------------------------------
 * QUICK SEARCH
 *
 * The leading icon sits INSIDE the field rather than in a separate bordered
 * box, so the control reads as one input rather than as a button glued to a
 * text box.
 * ------------------------------------------------------------------------ */
.quick-search .input-group-text {
    /* LOGICAL, not physical. padding-right pins the gap to the visual right,
     * which in RTL is the wrong side of the icon — the search glyph would sit
     * flush against the field it introduces. RTL is a hard requirement here,
     * so every directional value in this file is written in logical terms. */
    padding-inline-end: 0.25rem;
}

.quick-search .form-control:focus {
    box-shadow: none;
}

.quick-search .input-group:focus-within {
    border-radius: 0.25rem;
    box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15);
}

/* ---------------------------------------------------------------------------
 * PROTOTYPE — throwaway, removed with prototype_toolbar.templ.
 * ------------------------------------------------------------------------ */

/* The reference's search field: ONE bordered pill with the icon inside it,
 * rather than an input with a button welded to its edge. */
.pt-search {
    position: relative;
    display: inline-flex;
    align-items: center;
    min-width: 18rem;
    flex: 0 1 22rem;
}
.pt-search i {
    position: absolute;
    left: 0.75rem;
    color: #9aa0a6;
    font-size: 0.8rem;
    pointer-events: none;
}
.pt-search input {
    width: 100%;
    height: 38px;
    padding: 0 0.9rem 0 2.1rem;
    border: 1px solid #e3e5e8;
    border-radius: 10px;
    font-size: 0.875rem;
    background: #fff;
}
.pt-search input:focus {
    outline: none;
    border-color: #b9c6ff;
    box-shadow: 0 0 0 3px rgba(80, 110, 255, 0.12);
}

/* Light, evenly-weighted secondary buttons — the reference's chrome is almost
 * white, so the DATA carries the visual weight rather than the toolbar. */
.pt-a-btn,
.pt-filter-btn {
    height: 38px;
    border: 1px solid #e3e5e8;
    border-radius: 10px;
    background: #fff;
    color: #3c4043;
    font-size: 0.875rem;
    padding: 0 0.9rem;
}
.pt-a-btn:hover,
.pt-filter-btn:hover {
    background: #f6f7f9;
    color: #202124;
}

/* Tabs: the active one is marked by a weight change and a rule, not a filled
 * pill — a filled tab competes with the primary action for attention. */
.pt-a-tabs .nav-link {
    color: #5f6368;
    font-weight: 500;
    border-bottom: 2px solid transparent;
    padding-bottom: 0.6rem;
}
.pt-a-tabs .nav-link.active {
    color: #0d6efd;
    border-bottom-color: #0d6efd;
}

.pt-filter-panel {
    min-width: 22rem;
    max-width: 26rem;
    border: 1px solid #e3e5e8;
    border-radius: 12px;
}

.pt-c-chip {
    background: #eef2ff;
    color: #3b4cca;
    font-weight: 500;
    padding: 0.4rem 0.7rem;
}

/* Deliberately loud: nobody should mistake the switcher for the design. */
.pt-switcher {
    position: fixed;
    left: 50%;
    bottom: 1.25rem;
    transform: translateX(-50%);
    z-index: 1080;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.4rem 0.75rem;
    border-radius: 999px;
    background: #111;
    color: #fff;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    font-size: 0.8rem;
}
.pt-switcher-arrow {
    color: #fff;
    text-decoration: none;
    font-size: 1.1rem;
    line-height: 1;
    padding: 0 0.35rem;
}
.pt-switcher-label { white-space: nowrap; }

/* ---------------------------------------------------------------------------
 * CONTROL BAR — four feature groups
 * ------------------------------------------------------------------------ */

/* The bar has its own card again, so it sets no surface here — only rhythm. */
.grid-control-bar { margin-bottom: 0; }

/* UNIFORM ICON SQUARES, matching the PHP header.
 *
 * Equal widths are what let the group separators read as separators. With
 * mixed-width buttons the row looked like one undifferentiated strip and the
 * rules between groups were lost in it.
 *
 * The dropdown toggles are wider by the width of their caret, so the caret is
 * removed and the chevron dropped — the menus announce themselves by opening,
 * and a caret on a 34px square is most of the button. */
/* Scoped to the button GROUPS, not to a .btn-toolbar — the action container in
 * this bar is a plain flex div, and the first version of this rule targeted a
 * .btn-toolbar that only exists in the page header. It matched nothing, and the
 * buttons kept their carets and mixed widths while the CSS looked correct.
 *
 * The search and filter controls are deliberately NOT included: they carry
 * text, sit outside the action groups, and are a different kind of control. */
.grid-control-bar .btn-group > .btn,
.grid-control-bar .btn-group > .dropdown > .btn {
    /* FIXED width, not min-width. A dropdown toggle keeps padding for its
     * caret even when the caret is hidden, which left the toggles 2px wider
     * than the plain buttons — enough to make a row of "identical" squares
     * look subtly unaligned. */
    width: 36px;

    /* HEIGHT IS NOT SET. It derives from btn-sm, which is what every other
     * control in this bar derives from, and that is the whole point.
     *
     * It used to be a flat `height: 34px`. The search input, the Search button
     * and the Filter dropdown are all btn-sm and come out at 31px — 4px
     * padding, 21px line-height, 2px border — so the icon squares stood 3px
     * taller, 2px proud at the top and 1px below the others. On a row of
     * controls that reads as a wobble rather than as a measurement, and it is
     * exactly what a straight edge held under the bar shows.
     *
     * Restoring the vertical padding instead of pinning a number means the two
     * families track each other: if Bootstrap's btn-sm changes, or Chronos
     * retunes it, the squares follow. --bs-btn-padding-y is set by
     * `.btn-group-sm > .btn`, which these already match.
     *
     * Horizontal padding stays 0 because the fixed width positions the glyph.
     *
     * PADDING ALONE IS NOT ENOUGH, which the first attempt at this proved: the
     * button is inline-flex, so its height comes from the FLEX ITEM — the <i>
     * glyph, whose line box is shorter than a text button's. Restoring the
     * padding without the rest produced three heights instead of two (31 for
     * text, 24 for icons, 30 for the dropdown toggles).
     *
     * min-height reconstructs btn-sm's box from btn-sm's own variables:
     * 1.5em is the .btn line-height against --bs-btn-font-size, plus the
     * vertical padding, plus both borders. Same 31px, derived rather than
     * pinned. */
    /* SMALL SIZING PINNED HERE, not inherited from .btn-group-sm.
     *
     * Bootstrap's `.btn-group-sm > .btn` is a DIRECT-CHILD selector, and the
     * columns/views toggles live in a nested .btn-group inside the small one —
     * so they never matched it and kept the default 1rem/0.375rem variables.
     * With the height derived from those variables they came out at 38px while
     * everything beside them was 31px, which is the same wobble one layer down.
     *
     * Setting the variables on every action button in this bar makes the
     * sizing independent of how the groups happen to be nested. */
    --bs-btn-padding-y: 0.25rem;
    --bs-btn-font-size: 0.875rem;

    padding-block: var(--bs-btn-padding-y);
    padding-inline: 0;
    min-height: calc(1.5em + (2 * var(--bs-btn-padding-y)) + (2 * var(--bs-btn-border-width, 1px)));

    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.grid-control-bar .btn-group .dropdown-toggle::after { display: none; }

/* The Save-view form inside the Views panel is not an action square. */
.grid-control-bar .btn-group .dropdown-menu .btn { width: auto; height: auto; padding: revert; }

/* The primary action is distinguished by COLOUR, not by carrying the only
 * label — which is how PHP does it too. */
.grid-control-bar .btn-group > .btn-primary { width: 40px; }

/* The separator between groups. A hairline rather than a gap, because a gap
 * alone reads as "these wrapped", not "these are different kinds of thing". */
.control-bar-sep {
    display: inline-block;
    width: 1px;
    height: 22px;
    background: #dee2e6;
    margin: 0 0.15rem;
}

/* Filter chips: what is narrowing the list, stated rather than counted. */
.filter-chip {
    background: #eef2ff;
    color: #3b4cca;
    font-weight: 500;
    padding: 0.35rem 0.65rem;
    font-size: 0.78rem;
}
.filter-chip a:hover { opacity: 0.7; }

/* The chip row is the one part of the control bar that was never themed: two
 * hardcoded light values, and no dark variant, so on a dark grid the chips came
 * back as light-blue tablets against the dark bar. Legible, but the only thing
 * up there that did not follow the theme.
 *
 * It went unnoticed while chips were an occasional thing that appeared when a
 * filter was set. They are now the single surface saying what narrows the list
 * — including the search term — so they are on screen far more often.
 *
 * Same subtle-tint pairing Bootstrap uses for its own dark badges, rather than
 * inverting to the light values: a saturated chip would outweigh the rows it
 * describes. */
[data-bs-theme="dark"] .filter-chip {
    background: rgba(99, 102, 241, 0.18);
    color: #c7d2fe;
}

/* The filter popover. Capped in height so a page with twelve filters scrolls
 * inside the panel rather than running off the bottom of the viewport. */
.filter-popover {
    min-width: 22rem;
    max-width: 26rem;
    max-height: 70vh;
    overflow-y: auto;
}

/* ---------------------------------------------------------------------------
 * COLUMN HEADERS — two targets in one cell
 *
 * The name opens the menu, the indicator sorts. Both need to look clickable
 * without looking like two separate controls bolted together, so neither
 * carries a border and both take the same hover treatment.
 * ------------------------------------------------------------------------ */
.col-header-label {
    background: none;
    border: 0;
    padding: 0;
    /* A short label ("ID") is a 14px-wide target otherwise; 24px is the floor
     * for a pointer target (WCAG 2.5.8). */
    min-width: 24px;
    min-height: 24px;
    font: inherit;
    color: inherit;
    /* start, not left: the header label must align to the reading edge, which
     * is the right-hand side in RTL. */
    text-align: start;
    cursor: pointer;
}
.col-header-label:hover,
.col-header-sort:hover {
    color: var(--bs-primary, #0d6efd);
}

/* The indicator needs a real hit area — an 8px arrow is not a target. The
 * padding is invisible but makes it comfortably clickable. */
.col-header-sort {
    color: inherit;
    text-decoration: none;
    padding: 0.25rem 0.2rem;
    line-height: 1;
    border-radius: 4px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    min-height: 24px;
}
.col-header-sort:focus-visible,
.col-header-label:focus-visible {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: 2px;
}

.col-header-menu {
    min-width: 12rem;
    font-size: 0.875rem;
}
/* THE MENU IS CLIPPED BY THE SCROLL CONTAINER, NOT PAINTED UNDER IT.
 *
 * .table-responsive carries overflow:auto, and overflow clipping IGNORES
 * z-index — which is why raising it changed nothing. Clipped content is also
 * not hit-testable, so elementFromPoint returned .card-body for the part of the
 * menu hanging below the header row.
 *
 * Measured on an EMPTY grid (the shortest card, so the worst case): the
 * one-item menu on the last column was fine because it never left the header
 * row, while the three-item menu on the first column had its tail cut. Testing
 * only the short one would have shown this as working.
 *
 * The real fix is data-bs-strategy="fixed" on the toggle: Popper then positions
 * the menu with position:fixed, which takes it out of the scrolling ancestor
 * entirely. The z-index below only settles order against the sticky header.
 */
.grid-view .col-header { position: relative; }
.grid-view .col-header .dropdown.show { z-index: 1060; }
.grid-view .col-header .dropdown-menu { z-index: 1060; }

/* Navbar avatar — a circle, not an ellipse.
 *
 * .rounded-circle only sets border-radius:50%. On a .btn the horizontal
 * padding (6px 12px) is wider than the vertical, so the element was 52x38 and
 * 50% radius rendered an oval. A circle needs equal width and height, which
 * means overriding the button padding rather than adding to it.
 *
 * flex-shrink:0 keeps it round when the navbar is tight — a shrinking width
 * with a fixed height is the same bug by another route.
 *
 * overflow:hidden is what clips the SSO profile <img> (a square photo, covered
 * into the circle) when the operator signs in through Google or Azure. It is
 * harmless to the initials span, which never overflows. */
.navbar-avatar {
    width: 38px;
    height: 38px;
    padding: 0;
    flex-shrink: 0;
    line-height: 1;
    overflow: hidden;
}

/* ---------------------------------------------------------------------------
 * THE GRID PANEL — trail, controls and grid as one slab
 *
 * Mirrors what PHP's grid pages have always rendered, measured on
 * ?r=dispositivo: a glass .modern-header with a 12px top radius and no bottom
 * border, .modern-filter-widget abutting it at zero margin, then the table.
 *
 * The Go page had a loose breadcrumb over two detached cards. Two consequences,
 * both visible: the trail read as pinned to the top of the window (7px below
 * the navbar with no padding of its own, where PHP's identical 7px gap puts the
 * text 20px lower because the panel has padding), and the page had no head —
 * chrome stacked on data with a 16px seam through the middle.
 *
 * WHY .modern-header IS NOT STYLED GLOBALLY HERE. The base rule genuinely is
 * missing from the Go tier — ModernHeaderWidget registers it INLINE through
 * registerCss, so what reaches chronos-ui-overrides.css, and therefore Go, is
 * only the twenty-odd descendant rules that assume a container somebody else
 * styled. Supplying it globally would also restyle the monitoring board and the
 * suspensions dashboard, which use the same component and are not part of this
 * composition. Scoped to the panel, it changes exactly the pages it is about.
 * ------------------------------------------------------------------------- */
.grid-panel > .modern-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 1rem;
    padding: 0.75rem 1rem;

    /* Glass, matching the navbar and sidebar. DESIGN.md is explicit that this
     * is functional chrome rather than ornament: the panel is the frame around
     * the data, and it reads as chrome precisely because it shares the
     * material the other chrome uses. */
    background: rgba(255, 255, 255, 0.85);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid rgba(0, 0, 0, 0.08);

    /* No bottom border, and the radius only on top: the band below continues
     * the same box. A bottom border here would draw a line the eye reads as an
     * ending, which is the whole difference between a head and a stack. */
    border-bottom: none;
    border-radius: 12px 12px 0 0;
}

/* A WRAPPER THAT DOES NOT COUNT AS A LEVEL.
 *
 * The rules below are keyed on a DIRECT child, deliberately: a nested panel
 * must not inherit its parent's band chrome. But a page sometimes has to put
 * an element between the two — the report builder wraps its band in the form
 * carrying the report's state, and its body in the element htmx swaps. Both
 * were then styled by nothing at all: no padding, no border, no background,
 * measured against every sibling grid at 0px versus 8px. The band simply
 * looked cramped, which is how it was reported.
 *
 * This class marks such a wrapper as structural, so it is written into each
 * selector rather than the rules being loosened to any descendant.
 */
.grid-panel > .grid-panel-passthrough > .grid-panel-band,
/* The controls continue the panel: same borders, no radius, no seam. */
.grid-panel > .grid-panel-band {
    padding: 0.5rem 1rem;
    background: var(--bs-body-bg);
    border-inline: 1px solid rgba(0, 0, 0, 0.08);
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}

/* The grid closes it. Radius on the BOTTOM only, mirroring the header's top. */
.grid-panel > .grid-panel-passthrough > .grid-panel-body,
.grid-panel > .grid-panel-body {
    background: var(--bs-body-bg);
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-top: none;
    border-radius: 0 0 12px 12px;

    /* EXPLICIT PER AXIS, and both matter for different reasons.
     *
     * overflow-y: hidden clips the last row's background to the rounded
     * bottom; without it the table paints square corners over the panel's.
     *
     * overflow-x MUST STAY auto. This element also carries .table-responsive,
     * and the Actions column is off-screen below about 900px — a plain
     * `overflow: hidden` here takes the horizontal scroll with it and makes
     * edit and delete unreachable, which is the exact failure the wrapper's
     * own comment in ui/resource.templ was added to prevent. Written per axis
     * rather than relying on .table-responsive to win the cascade: it does
     * today, and that is luck rather than a guarantee. */
    overflow-x: auto;
    overflow-y: hidden;
}

/* Dark parity is not optional (PRODUCT.md). Values taken from PHP's own dark
 * rule so the two tiers render the same panel rather than two near-misses. */
[data-bs-theme="dark"] .grid-panel > .modern-header {
    background: rgba(26, 29, 35, 0.85);
    border-color: rgba(255, 255, 255, 0.08);
}

[data-bs-theme="dark"] .grid-panel > .grid-panel-passthrough > .grid-panel-band,
[data-bs-theme="dark"] .grid-panel > .grid-panel-passthrough > .grid-panel-body,
[data-bs-theme="dark"] .grid-panel > .grid-panel-band,
[data-bs-theme="dark"] .grid-panel > .grid-panel-body {
    border-color: rgba(255, 255, 255, 0.08);
}

/* The control bar carried its own bottom spacing when it was a card body.
 * Inside the panel the band's padding is the spacing, and the doubled gap
 * showed as a slack strip above the table. */
.grid-panel > .grid-panel-passthrough > .grid-panel-band .grid-control-bar,
.grid-panel > .grid-panel-band .grid-control-bar {
    margin-bottom: 0;
}

/* The trail's own bottom margin is not its job any more.
 *
 * ui.Breadcrumbs carries mb-2 on its wrapper to hold the trail off whatever
 * follows it — correct when it was a loose band above a card, wrong inside a
 * panel whose padding already does that. It made the trail sit OPTICALLY HIGH
 * in its own band: measured 13px above, 20px below. The eye reads that as the
 * header being misaligned rather than as 8px of margin.
 *
 * !important because .mb-2 is a BOOTSTRAP UTILITY, and utilities carry it — a
 * scoped rule without it loses to a class sitting in the markup. Narrow enough
 * to be honest: one element, inside one panel. */
.grid-panel > .modern-header .modern-breadcrumb {
    margin-bottom: 0 !important;
}

/* ---------------------------------------------------------------------------
 * FORM FIELDS RESERVE THEIR VALIDATION LINE
 *
 * A dialog form that grows when it is rejected re-centres itself, so the fields
 * the operator is reading move under the cursor at the moment they are being
 * asked to look at them. Measured on the message-template dialog: submitting it
 * empty grew the modal by 83px — 50 of that a banner that has since gone, the
 * rest the field message appearing.
 *
 * The space is reserved here rather than by rendering an empty feedback element
 * always: an unconditional .invalid-feedback is what crushed checkbox labels in
 * the PHP tier, where the global .form-check{display:flex} gave it a flex slot.
 * ------------------------------------------------------------------------- */
.field-slot {
    position: relative;
    padding-bottom: 1.35rem;
}

.field-slot > .invalid-feedback,
.field-slot .invalid-feedback.d-block {
    position: absolute;
    inset-inline-start: 0;
    bottom: 0;
    margin-top: 0;
}

/* A FILE FIELD OPTS OUT OF THE RESERVED LINE, and it is the one kind that
 * should.
 *
 * The reservation above pins the message to a single line at the bottom of the
 * slot. That fits every other kind, whose refusals are phrases — "Name is
 * required.", "That is not a date." A file control's are SENTENCES, because
 * they have to say what was wrong AND what is allowed: "That image cannot be
 * accepted: SVG and other XML images can carry scripts." At a half-width
 * dialog column that wraps to two lines, and an absolutely-positioned two-line
 * message printed itself straight over the help text above it — measured in a
 * browser on 2026-09-10, on the SVG refusal, where the sentence explaining a
 * security decision was the thing rendered illegible.
 *
 * So the message flows, and the dialog grows by a line when one appears. That
 * trade is deliberate and it is the opposite of the one the rule above takes:
 * reserving for the worst case would leave two blank lines under every file
 * control on every render, and pinning to one line makes the message
 * unreadable exactly when it is being read. Unreadable is worse than moving.
 *
 * :has() rather than a modifier class on .field-slot, so this is scoped
 * entirely to the file field's own markup and no other kind's slot changes. */
.field-slot:has(.upload-field) {
    padding-bottom: 0;
}

.field-slot:has(.upload-field) .invalid-feedback.d-block {
    position: static;
}

/* ---------------------------------------------------------------------------
 * TOMSELECT: THE CHEVRON IS PART OF THE CONTROL
 *
 * Clicking the arrow — the one part of a select everybody aims at — did nothing.
 * Measured on the message-template dialog: the .ts-wrapper was 376px wide and
 * the .ts-control that opens the dropdown only 326px, so elementFromPoint 18px
 * in from the right edge returned the WRAPPER. The last 37px were dead.
 *
 * TomSelect's own bootstrap5 theme gets this right — it sets
 * `.ts-wrapper.form-select { padding: 0 !important }` so the control fills the
 * box. chronos-ui-overrides.css then puts it back:
 *
 *   :where(html:not([dir="rtl"]), …) .ts-wrapper.form-select.single {
 *       background-position: right 0.75rem center !important;
 *       padding-right: 2.25rem !important;
 *       padding-left: 0.75rem !important;
 *   }
 *
 * That reserves the chevron's room on the WRAPPER, which is padding the control
 * cannot occupy. The rule is the PHP tier's and is loaded on Go pages too; this
 * neutralises the padding for the Go tier only — same specificity, same
 * !important, later in the cascade — and leaves background-position alone, so
 * the chevron still draws where it did. The control is transparent, so a
 * background image cannot swallow a click, and TomSelect already reserves the
 * caret's space INSIDE the control via --ts-pr-caret.
 *
 * Both directions, because the RTL half of that rule pads the other side.
 * ------------------------------------------------------------------------- */
:where(html, body) .ts-wrapper.form-select.single,
:where(html, body) .ts-wrapper.form-select.single:not(.rtl),
:where(html, body) .ts-wrapper.form-select.single.rtl {
    padding-right: 0 !important;
    padding-left: 0 !important;
}

.ts-wrapper.form-select > .ts-control {
    /* Fills the wrapper it sits in, so every pixel of the control opens it. */
    width: 100%;
    background: transparent;
    border: 0;
    border-radius: inherit;
    box-shadow: none;
    cursor: pointer;
}

/* ---------------------------------------------------------------------------
 * A TomSelect dropdown that escaped its scroll container.
 *
 * TomSelectController sets dropdownParent: 'body' for a control inside a
 * scrollable ancestor, so the list is no longer clipped by .modal-body — see
 * the note there. Body-parenting moves it out of the modal's stacking context
 * too, so it needs a z-index of its own: Bootstrap paints .modal at 1055, and
 * without this the list renders BEHIND the dialog that opened it — a dropdown
 * that appears to do nothing at all.
 *
 * `body >` is the whole selector on purpose. It matches only the escaped ones;
 * every in-place dropdown keeps the library's own stacking.
 * ------------------------------------------------------------------------- */
body > .ts-dropdown {
    /* 1101, and every digit of it was measured rather than assumed.
     *
     * A shown modal in Chronos computes to z-index 1100, not Bootstrap's 1055.
     * Three rules stack up and the LAST one only applies once the dialog is
     * open, which is what makes this easy to get wrong:
     *
     *   bootstrap.min.css        .modal       var(--bs-modal-zindex)  = 1055
     *   chronos-ui-overrides.css .modal       1060
     *   EBootstrapDialog.css     .modal.show  1100   <- wins when open
     *
     * This was first written as 1060 - the middle value, read off a CLOSED
     * modal - so every escaped dropdown opened BEHIND the dialog that owned
     * it. The list was present, positioned and populated, and invisible: the
     * bug reported as "the dropdowns never load any option". Nothing in the
     * DOM looked wrong, which is why it took a hit-test at the dropdown's own
     * centre to see a form row inside the modal painting over it.
     *
     * If EBootstrapDialog.css ever moves .modal.show, this moves with it. */
    z-index: 1101;
}

/* ---------------------------------------------------------------------------
 * modal-dialog-scrollable, restored for a dialog whose body is inside a <form>.
 *
 * Bootstrap makes the body scroll by a flex chain: .modal-content is a flex
 * COLUMN with overflow:hidden, and .modal-body is a direct flex child with
 * flex:1 1 auto, so it gets a bounded height and scrolls inside it.
 *
 * ResourceDialog breaks that chain, and has since it was written: the body and
 * footer live inside a <form>, so the form is the flex child and the body is
 * just a block inside it. A block grows to its content, so nothing scrolls and
 * .modal-content's overflow:hidden CLIPS the overflow instead.
 *
 * Measured on the person create dialog at an 820px window: .modal-content was
 * 621px tall holding 1027px of content, and .modal-body reported height 872px
 * with scrollHeight 872 — not scrolling, simply overflowing, its top 159px
 * ABOVE the dialog. The last section and the custom fields were unreachable.
 * Invisible until a dialog is taller than the window, which is why a tall form
 * is what surfaced it.
 *
 * Making the form a flex column with min-height:0 re-links the chain: the form
 * takes the space .modal-body used to, and the body becomes the bounded,
 * scrolling child again. min-height:0 is load-bearing — a flex item's default
 * min-height:auto refuses to shrink below its content and the overflow returns.
 * ------------------------------------------------------------------------- */
.modal-dialog-scrollable .modal-content > form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

/* ---------------------------------------------------------------------------
 * The reusable image-upload control.
 *
 * Sized in rem and themed through Bootstrap's own custom properties, so it
 * follows dark mode without a second palette — --bs-body-bg and --bs-border-color
 * are already redefined per theme by the framework.
 * ------------------------------------------------------------------------- */
.image-upload-zone {
    display: flex;
    align-items: center;
    gap: 1rem;
    width: 100%;
    padding: 1rem;
    border: 2px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius-lg, 0.5rem);
    background: var(--bs-body-bg);
    cursor: pointer;
    /* Colour and border only — never a layout property. A transition on
     * padding or border-width makes the whole form nudge on hover. */
    transition: border-color 0.15s ease-out, background-color 0.15s ease-out;
}

.image-upload-zone:hover,
.image-upload-zone:focus-within {
    border-color: var(--bs-primary);
}

/* The drag state has to be visibly different from hover: a pointer dragging a
 * file is already hovering, so the two would otherwise be indistinguishable at
 * the moment feedback matters most. */
.image-upload-zone.is-dragging {
    border-color: var(--bs-primary);
    background: var(--bs-primary-bg-subtle);
}

.image-upload-zone.is-busy {
    opacity: 0.6;
    pointer-events: none;
}

.image-upload-preview {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 5rem;
    height: 5rem;
    overflow: hidden;
    border-radius: 50%;
    background: var(--bs-secondary-bg);
}

.image-upload-img {
    width: 100%;
    height: 100%;
    /* cover, not contain: this previews a round avatar, and letterboxing a
     * portrait inside a circle shows the operator something the page will not. */
    object-fit: cover;
}

/* The stored photo, whole: contained rather than covered, capped so a
 * portrait does not push the drop zone below the fold of a modal-md. */
.image-upload-current {
    display: flex;
    justify-content: center;
    margin-bottom: 1rem;
}

.image-upload-full {
    max-width: 100%;
    max-height: 22rem;
    object-fit: contain;
    border-radius: var(--bs-border-radius-lg, 0.5rem);
    background: var(--bs-secondary-bg);
}

.image-upload-icon {
    font-size: 1.5rem;
    color: var(--bs-secondary-color);
}

.image-upload-copy {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    min-width: 0;
}

.image-upload-primary {
    font-size: 0.875rem;
    color: var(--bs-body-color);
}

.image-upload-hint,
.image-upload-status {
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
}

/* RESERVED WHETHER OR NOT IT SAYS ANYTHING. An empty status line that appears
 * only on an error pushes everything below it down at the worst moment — the
 * same shift the form dialog's error banner was fixed for. */
.image-upload-status {
    min-height: 1.25rem;
    margin-top: 0.5rem;
}

.image-upload-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.25rem;
}

@media (prefers-reduced-motion: reduce) {
    .image-upload-zone {
        transition: none;
    }
}

/* ---------------------------------------------------------------------------
 * The grid's avatar cell.
 *
 * The photo and the initials are BOTH in the markup; whichever is on top wins.
 * AvatarFallbackController adds .avatar-broken to the wrapper when the image
 * 404s, which reveals the initials underneath — the same mechanism the
 * monitoring avatars use, and the reason the server does not stat 25 files to
 * render one page.
 * ------------------------------------------------------------------------- */
.grid-avatar-cell {
    width: 3rem;
}

.grid-avatar-cell .avatar-circle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    overflow: hidden;
    border-radius: 50%;
    background: var(--bs-secondary-bg);
}

.grid-avatar-initials {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
}

/* The photo covers the initials while it loads and after it succeeds. */
.grid-avatar-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}

/* … and stops covering them the moment it fails. */
.avatar-broken .grid-avatar-img {
    display: none;
}

/* ---------------------------------------------------------------------------
 * The navbar omnibox's two-pane palette — docs/design/global-search-go.md.
 *
 * Shaped after the Tailwind Plus "command palette with preview" block: one
 * opaque card, the result list on the left, a preview column on the right that
 * leads with a centred avatar, and a muted "Create" bar across the bottom.
 *
 * EVERY PROPERTY IS !important AND EXPLICIT. The page loads several published
 * bundles that can style a `.dropdown-menu`, a `.form-control` and generic
 * `div`s; a lost rule here is what left the panes at page scale.
 *
 * THE PANEL PAINTS THE CARD, not its children. The children used to each set
 * `background: var(--bs-dropdown-bg)`, and Bootstrap 5.3 scopes that token to
 * `.dropdown-menu` itself — so on the preview and the footer, which are not
 * dropdown menus, it resolved to nothing. `background` fell back to
 * transparent and the `border: 1px solid var(--bs-dropdown-border-color)`
 * shorthands became invalid-at-computed-value-time, which drops the whole
 * declaration and leaves border-style `none` (a computed width of 0px). Both
 * panes rendered see-through and borderless over the dashboard. The tokens
 * used here — `--bs-body-bg`, `--bs-border-color`, `--bs-secondary-color` —
 * are :root-level and follow the dark-mode swap.
 * ------------------------------------------------------------------------- */
.global-search-panel {
    display: none;
    position: absolute !important;
    top: calc(100% + 0.5rem) !important;
    /* Logical, not left/right: the panel hangs off the start edge of the
     * search field, which is the RIGHT edge under an RTL locale. */
    inset-inline-start: 0 !important;
    inset-inline-end: auto !important;
    width: 880px !important;
    max-width: 95vw !important;
    min-width: 0 !important;
    z-index: 1050 !important;
    background: var(--bs-body-bg) !important;
    border: 1px solid var(--bs-border-color) !important;
    border-radius: var(--bs-border-radius-lg) !important;
    box-shadow: var(--bs-box-shadow-lg) !important;
    overflow: hidden !important;
}

.global-search-panel:has(.dropdown-menu.show) {
    display: flex !important;
    flex-wrap: wrap !important;
    align-items: stretch !important;
}

/* Sites carry no preview, so the pane hides — and an 880px panel would leave
 * a 280px column of empty card beside the list. Shrink to the list's width. */
.global-search-panel:has(.global-search-preview.d-none) {
    width: 600px !important;
}

/* POSITION IS THE LOAD-BEARING ONE. `.dropdown-menu` is `position: absolute`
 * in Bootstrap's base layer, and this block overrode everything about the list
 * EXCEPT that — so it left the flex flow entirely and painted, opaque and
 * shadowed, on top of its own siblings. The preview pane sat underneath it at
 * the same origin and the footer underneath its lower half; both rendered
 * their content correctly and neither was ever visible. */
#global-search-results.dropdown-menu {
    position: static !important;
    inset: auto !important;
    transform: none !important;
    float: none !important;
    flex: 0 0 600px !important;
    width: 600px !important;
    max-width: 600px !important;
    min-width: 0 !important;
    max-height: 480px !important;
    overflow-y: auto !important;
    margin: 0 !important;
    padding: 0.5rem !important;
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
}

/* Sticky, so the operator scrolling a long People group can still see which
 * group they are in. */
#global-search-results .dropdown-header {
    position: sticky !important;
    top: 0 !important;
    z-index: 1 !important;
    font-size: 0.75rem !important;
    font-weight: 600 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.03em !important;
    color: var(--bs-secondary-color) !important;
    background: var(--bs-body-bg) !important;
    padding: 0.75rem 0.75rem 0.25rem !important;
}

#global-search-results .dropdown-item {
    font-size: 0.875rem !important;
    padding: 0.5rem 0.75rem !important;
    border-radius: var(--bs-border-radius) !important;
    white-space: nowrap !important;
    color: var(--bs-body-color) !important;
}

/* THE SELECTED ROW IS A TINT, NOT A SLAB OF PRIMARY. `.dropdown-item.active`
 * is solid --bs-primary with white text in Bootstrap's base layer, which in a
 * list where the selection moves under the pointer reads as a click that has
 * already happened, drags the eye off the preview pane the row is describing,
 * and puts a white-on-blue row next to badges that are themselves saturated.
 * A tint plus a start-edge accent says "this one" at the weight the job needs.
 *
 * The three states are ONE rule on purpose: the controller sets `.active` on
 * hover, on arrow-key move and on focus, so hover and keyboard can never show
 * two different rows as selected. */
#global-search-results .dropdown-item.active,
#global-search-results .dropdown-item:hover,
#global-search-results .dropdown-item:focus {
    background-color: var(--bs-secondary-bg) !important;
    color: var(--bs-emphasis-color) !important;
    box-shadow: inset 3px 0 0 0 var(--bs-primary) !important;
    outline: 0 !important;
}

/* Keyboard focus still needs to be distinguishable from a hover, for anyone
 * driving this without a pointer. */
#global-search-results .dropdown-item:focus-visible {
    outline: 2px solid var(--bs-primary) !important;
    outline-offset: -2px !important;
}

/* RTL-safe: the accent belongs on the start edge, and `inset 3px` is the
 * left one whatever the direction. */
[dir="rtl"] #global-search-results .dropdown-item.active,
[dir="rtl"] #global-search-results .dropdown-item:hover,
[dir="rtl"] #global-search-results .dropdown-item:focus {
    box-shadow: inset -3px 0 0 0 var(--bs-primary) !important;
}

#global-search-results .dropdown-item .badge {
    font-weight: 600 !important;
    font-size: 0.6875rem !important;
    border-radius: 10rem !important;
    padding: 0.25em 0.6em !important;
}

#global-search-results .dropdown-item .text-truncate {
    max-width: 18rem !important;
}

.global-search-preview {
    align-self: stretch !important;
    flex: 1 1 260px !important;
    /* EXPLICIT, because the pane inherits from the navbar. The values carried
     * no colour of their own and picked up the header's, which is a fixed dark
     * — legible on the light card and all but invisible on the dark one. Every
     * text colour in this pane is now named against a :root token that follows
     * the theme. Caught in dark mode; it was never visible in light. */
    color: var(--bs-body-color) !important;
    width: auto !important;
    max-width: none !important;
    min-width: 260px !important;
    max-height: 480px !important;
    overflow-y: auto !important;
    padding: 1.25rem 1rem !important;
    background: var(--bs-tertiary-bg) !important;
    border: 0 !important;
    border-inline-start: 1px solid var(--bs-border-color) !important;
    border-radius: 0 !important;
}

/* The reference leads the pane with the record itself — avatar, name, kind —
 * so the operator reads what they are pointing at without tracking back to the
 * highlighted row. */
.global-search-preview-head {
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    text-align: center !important;
    gap: 0.25rem !important;
    padding-bottom: 1rem !important;
    margin-bottom: 1rem !important;
    border-bottom: 1px solid var(--bs-border-color) !important;
}

.global-search-preview-photo,
.global-search-preview-initials {
    width: 4rem !important;
    height: 4rem !important;
    border-radius: 50% !important;
    margin-bottom: 0.25rem !important;
}

.global-search-preview-photo {
    object-fit: cover !important;
}

/* Shown INSTEAD of the photo when there is none. personPreview addresses
 * every person's photo unconditionally — most have none, and the browser's
 * broken-image glyph is a worse answer than their initials. */
.global-search-preview-initials {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    font-size: 1.25rem !important;
    font-weight: 600 !important;
    color: var(--bs-secondary-color) !important;
    background: var(--bs-secondary-bg) !important;
}

.global-search-preview-name {
    font-size: 0.95rem !important;
    font-weight: 600 !important;
    line-height: 1.3 !important;
    color: var(--bs-emphasis-color) !important;
    word-break: break-word !important;
}

.global-search-preview-head .badge {
    font-weight: 600 !important;
    font-size: 0.6875rem !important;
    border-radius: 10rem !important;
    padding: 0.25em 0.6em !important;
}

.global-search-preview-rows {
    display: flex !important;
    flex-direction: column !important;
    gap: 0.7rem !important;
}

.global-search-preview-item {
    display: flex !important;
    flex-direction: column !important;
    gap: 0 !important;
}

.global-search-preview-label {
    font-size: 0.75rem !important;
    line-height: 1.2 !important;
    color: var(--bs-secondary-color) !important;
    margin: 0 !important;
    padding: 0 !important;
}

.global-search-preview-value {
    color: var(--bs-body-color) !important;
    font-size: 0.9rem !important;
    line-height: 1.3 !important;
    margin: 0 !important;
    padding: 0 !important;
    word-break: break-word !important;
}

.global-search-footer {
    flex: 0 0 100% !important;
    display: flex !important;
    align-items: center !important;
    flex-wrap: wrap !important;
    gap: 0.375rem !important;
    padding: 0.625rem 0.75rem !important;
    border-top: 1px solid var(--bs-border-color) !important;
    font-size: 0.875rem !important;
    background: var(--bs-tertiary-bg) !important;
}

.global-search-footer-label {
    color: var(--bs-secondary-color) !important;
    font-size: 0.75rem !important;
    font-weight: 600 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.03em !important;
    margin-inline-end: 0.25rem !important;
}

/* PILLS, not a row of blue links. Four underlined links in a line read as
 * prose and gave no target big enough to aim at; a pill is a button-shaped
 * thing that says it can be pressed. They stay <a> elements — each one is a
 * real destination, and Tab reaches them after the last result. */
.global-search-create {
    display: inline-flex !important;
    align-items: center !important;
    gap: 0.375rem !important;
    padding: 0.25rem 0.75rem !important;
    border: 1px solid var(--bs-border-color) !important;
    border-radius: 10rem !important;
    background: var(--bs-body-bg) !important;
    color: var(--bs-body-color) !important;
    font-size: 0.8125rem !important;
    line-height: 1.4 !important;
    text-decoration: none !important;
    transition: background-color 0.12s ease, border-color 0.12s ease !important;
}

/* The leading plus is a pseudo-element so the four pills stay one templ line
 * each. FontAwesome 7 PRO is what this app loads — Free would render a blank
 * box for a Pro glyph, and \f067 (plus) exists in both, which is why it is
 * safe here specifically. */
.global-search-create::before {
    content: "\f067";
    font-family: "Font Awesome 7 Pro", "Font Awesome 6 Pro", sans-serif !important;
    font-weight: 900 !important;
    font-size: 0.625rem !important;
    opacity: 0.65 !important;
}

.global-search-create:hover,
.global-search-create:focus-visible {
    background: var(--bs-secondary-bg) !important;
    border-color: var(--bs-secondary-color) !important;
    color: var(--bs-emphasis-color) !important;
}

.global-search-create:focus-visible {
    outline: 2px solid var(--bs-primary) !important;
    outline-offset: 2px !important;
}

/* Below md the palette collapses to the single-pane list it always was. */
@media (max-width: 767.98px) {
    .global-search-panel,
    .global-search-panel:has(.global-search-preview.d-none) {
        width: 100% !important;
        max-width: 100% !important;
    }

    .global-search-preview {
        display: none !important;
    }

    #global-search-results.dropdown-menu {
        flex: 1 1 100% !important;
        width: 100% !important;
        max-width: 100% !important;
    }
}

/* ---------------------------------------------------------------------------
 * DIRECTION IS DECIDED BY THE CONTENT, NOT BY THE PAGE — data values only.
 *
 * Under an RTL locale a run with no strong character inherits the paragraph
 * direction, and the bidi algorithm reorders it. Measured on ar_ma, live:
 *
 *   02:00–04:10                  ->  04:10–02:00      (a CURFEW WINDOW, x12)
 *   609616239-Automation         ->  Automation-609616239
 *   616 St Kilda Rd, Melbourne   ->  St Kilda Rd, Melbourne 616
 *   109093764, 209920284, 409..  ->  409.., 209920284, 109093764
 *   1–25 of 8538                 ->  of 8538 25–1
 *
 * None of these is truncated or visibly broken. They are plausible, wrong
 * values in the places an operator reads a value off the screen, and the
 * curfew one states a compliance window backwards.
 *
 * `unicode-bidi: plaintext` is the CSS spelling of `dir="auto"`: each
 * paragraph takes its direction from its own first strong character. Times and
 * counts have none and fall to LTR; a Latin identifier anchors LTR; an ARABIC
 * value still resolves RTL and is untouched — verified by measuring six
 * Arabic cells before and after, none of which moved.
 *
 * SCOPED TO VALUES. Headers and prose are translated text and keep the page
 * direction. This does reach a PHP fragment rendered inside a Go dialog, which
 * is harmless and fixes the same class there.
 *
 * NOT a substitute for SearchHit.LTRLabel: plaintext anchors on the first
 * strong character, and a SIM label leads with the TRANSLATED word for Device,
 * so it would resolve that row to RTL. Where a translated word can precede an
 * identifier, the direction has to be stated outright.
 * ------------------------------------------------------------------------- */
.table td,
.grid-summary,
.program-value,
.schedule-entry-time {
    unicode-bidi: plaintext;
}

/* ---------------------------------------------------------------------------
 * REPORT GRID COLUMN ORDER
 *
 * Ordering lives where its result is visible: in the table header. The grip is
 * the pointer path and the adjacent arrows are the keyboard/touch path. The
 * header itself is not draggable, so its sort link remains an ordinary link.
 * ------------------------------------------------------------------------- */
.report-column-drag-handle {
    cursor: grab;
    padding-inline: 0.15rem;
    /* Smaller than the heading it sits beside: it is a handle, not a heading. */
    font-size: 0.75rem;
}

.report-column-drag-handle:active {
    cursor: grabbing;
}

.report-column-dragging {
    opacity: 0.55;
}

/* REVEALED ON HOVER, LIKE THE FIELD PICKER'S. A heading is read for its name;
 * four marks standing beside every one of them turned the header row into a
 * bank of controls with the names in between. :focus-within keeps them for a
 * keyboard — opacity alone would leave somebody tabbing to a control they
 * cannot see. They keep their space when hidden, so nothing shifts as the
 * pointer crosses the row. */
.report-column-controls {
    white-space: nowrap;
    opacity: 0;
    transition: opacity 0.1s ease-in-out;
}

.report-column-header:hover .report-column-controls,
.report-column-header:focus-within .report-column-controls {
    opacity: 1;
}

/* NO UNDERLINE AND NO BUTTON CHROME. These are marks on a heading, not links
 * in a sentence; btn-link drew a rule under each of the four. */
.report-column-control {
    display: inline-block;
    padding-inline: 0.15rem;
    line-height: 1;
    color: var(--bs-secondary-color);
    text-decoration: none;
}

.report-column-control:hover,
.report-column-control:focus {
    color: var(--bs-body-color);
    text-decoration: none;
}

.report-column-control.disabled {
    opacity: 0.35;
    pointer-events: none;
}

/* ---------------------------------------------------------------------------
 * REPORT TOOLBAR — the controls band
 *
 * Each control opens a menu that owns one concern, and the menus hold real
 * forms rather than lists of links, so they need a floor and a ceiling: wide
 * enough for a filter row, short enough that a long column list scrolls inside
 * the menu instead of running off the screen.
 * ------------------------------------------------------------------------- */
.report-menu {
    /* Match the toggle's 4px Popper offset when rendered already open. */
    --bs-dropdown-spacer: 4px;
    min-width: min(22rem, calc(100vw - 1rem));
    max-width: calc(100vw - 1rem);
    max-height: 70vh;
    overflow-y: auto;
}

/* The add-filter choices expand in normal flow. Keeping their own sensible
 * ceiling means a large catalogue scrolls as a list of fields, rather than
 * turning the whole filter card into the clipping container of a nested
 * Bootstrap dropdown. */
.report-add-filter-options {
    max-height: 16rem;
    overflow-y: auto;
}

/* The band wraps rather than scrolls: on a narrow window the controls stack
 * into a second row, which keeps every one of them reachable. A horizontally
 * scrolling toolbar hides controls behind a gesture nobody looks for. */
.report-toolbar {
    row-gap: 0.5rem;
}

/* An empty report spans the whole table. The mobile card layout reserves half
 * of ordinary cells for labels, which would squeeze these recovery actions. */
#report-results td.report-empty {
    padding-inline: 1rem !important;
    text-align: center !important;
}

#report-results td.report-empty::before {
    display: none !important;
}

/* Keep column headings beside their values in the mobile card layout. Both
 * occupy normal flow so long custom headings wrap without covering the next
 * row. Logical alignment also follows RTL without a second positioning rule. */
@media screen and (max-width: 767px) {
    #report-results .responsive-table td[data-title] {
        display: grid !important;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: 0.5rem;
        padding: 0.5rem !important;
        text-align: start !important;
        overflow-wrap: anywhere;
    }

    #report-results .responsive-table td[data-title]::before {
        content: attr(data-title) !important;
        color: var(--bs-body-color) !important;
        position: static !important;
        width: auto !important;
        padding: 0 !important;
        white-space: normal !important;
        text-align: start !important;
    }
}

/* Keep the report identity and its actions together when the band wraps. */
.report-state-row,
.report-state-details,
.report-state-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.report-state-row {
    column-gap: 0.75rem;
    margin-block-end: 0.625rem;
}

.report-state-details {
    min-width: 0;
}

.report-state-name {
    min-width: 0;
    max-width: 32rem;
    color: var(--bs-body-color);
    font-size: 0.875rem;
    line-height: 1.4;
}

.report-state-name > i {
    color: var(--bs-secondary-color);
    font-size: 0.75rem;
}

.report-state-badge {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
    padding: 0.25rem 0.5rem;
    border: 1px solid var(--bs-warning-border-subtle);
    border-radius: 0.25rem;
    background-color: var(--bs-warning-bg-subtle);
    color: var(--bs-warning-text-emphasis);
    font-size: 0.6875rem;
    font-weight: 500;
    line-height: 1.2;
}

/* Compact actions sit just above the status badge in size and emphasis. */
.report-state-action {
    --bs-btn-padding-x: 0.625rem;
    --bs-btn-padding-y: 0.1875rem;
    --bs-btn-font-size: 0.8125rem;
    --bs-btn-font-weight: 500;
    --bs-btn-line-height: 1.25;
    --bs-btn-border-radius: 0.375rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    min-height: 1.75rem;
    white-space: nowrap;
}

.report-state-action > i {
    font-size: 0.75rem;
}

.report-state-action.report-state-reset {
    --bs-btn-font-weight: 500;
    --bs-btn-color: var(--bs-secondary-color);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--bs-emphasis-color);
    --bs-btn-hover-bg: var(--bs-tertiary-bg);
    --bs-btn-hover-border-color: var(--bs-border-color);
    --bs-btn-active-color: var(--bs-emphasis-color);
    --bs-btn-active-bg: var(--bs-secondary-bg);
    --bs-btn-active-border-color: var(--bs-border-color);
}

.report-saved-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
}

.report-saved-filters > .btn {
    flex: 1 1 auto;
    font-size: 0.75rem;
    padding: 0.25rem 0.375rem;
}

.report-saved-row {
    padding: 0.375rem 0.625rem;
    font-size: 0.8125rem;
}

.report-saved-name {
    min-width: 0;
    overflow-wrap: anywhere;
}

.report-saved-recent,
.report-saved-recent .list-group-item {
    background-color: var(--bs-tertiary-bg);
}

.report-favorite {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    --bs-btn-color: var(--bs-secondary-color);
    --bs-btn-hover-color: var(--bs-warning-text-emphasis);
    --bs-btn-hover-bg: var(--bs-warning-bg-subtle);
    --bs-btn-focus-shadow-rgb: var(--bs-warning-rgb);
}

.report-favorite.is-favorite {
    --bs-btn-color: var(--bs-warning-text-emphasis);
}

/* Grouping fields use compact, centered rows; the date interval belongs
 * below its selected field, so neither control squeezes the other's label. */
.report-group-option {
    padding: 0.25rem 0.625rem;
    margin-bottom: 0.25rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
}

.report-group-option.is-selected {
    border-color: var(--bs-primary-border-subtle);
    background: var(--bs-primary-bg-subtle);
}

.report-group-option .form-check {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0;
    min-height: 1.5rem;
}

.report-group-option .form-check-input {
    float: none;
    flex-shrink: 0;
    margin: 0;
}

.report-group-option .form-check-label {
    flex: 1;
    min-width: 0;
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.25rem;
    overflow-wrap: anywhere;
    cursor: pointer;
}

.report-group-interval {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.375rem 0.75rem;
    margin-block-start: 0.375rem;
    padding-inline-start: 1.5rem;
}

.report-group-interval .form-select {
    flex: 1 1 6rem;
    width: auto;
    min-width: 0;
}

.report-chart-choice {
    flex: 0 1 14rem;
    min-width: 0;
    max-width: 100%;
}

.report-chart-choice:last-child {
    flex-basis: 22rem;
}

@media (max-width: 575.98px) {
    .report-chart-choice,
    .report-chart-choice:last-child {
        flex: 1 1 100%;
    }
}

#report-chart-result .metric-card,
#report-chart-result .card-body {
    min-width: 0;
}

#report-chart-result .metric-card > .card-header {
    padding: 0.75rem 0.75rem 0.375rem;
}

#report-chart-result .metric-card > .card-body {
    padding: 0.375rem 0.75rem 0.75rem;
}

/* Apex positions axis titles using left-to-right SVG text anchors. */
#report-chart-result [data-apexchart-target="canvas"] {
    direction: ltr;
}

#report-chart-result .apexcharts-tooltip {
    white-space: normal;
    max-width: min(24rem, calc(100vw - 2rem));
    overflow-wrap: anywhere;
}

#report-chart-result .card-title,
#report-chart-result .chart-caption {
    overflow-wrap: anywhere;
}

.report-chart-legend {
    border-top: 1px solid var(--bs-border-color);
    margin-top: 1rem;
    padding-top: 0.75rem;
}

.report-chart-legend-items {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
    gap: 0.25rem 0.5rem;
    max-height: 15rem;
    overflow-y: auto;
    padding: 0.25rem;
}

.report-chart-legend-item {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
    min-height: 1.75rem;
    padding: 0.25rem 0.5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
    background: transparent;
    color: var(--bs-body-color);
    font: inherit;
    font-size: 0.75rem;
    line-height: 1.25;
    text-align: start;
}

.report-chart-legend-item:hover {
    background: var(--bs-tertiary-bg);
}

.report-chart-legend-item:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

.report-chart-swatch {
    flex: 0 0 0.625rem;
    height: 0.625rem;
    border-radius: 0.1875rem;
}

.report-chart-series-name {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
}

.report-chart-series-check {
    font-size: 0.625rem;
}

.report-chart-legend-item[aria-pressed="false"] .report-chart-series-name {
    color: var(--bs-secondary-color);
    text-decoration: line-through;
}

.report-chart-legend-item[aria-pressed="false"] .report-chart-swatch {
    opacity: 0.35;
}

.report-chart-legend-item[aria-pressed="false"] .report-chart-series-check {
    visibility: hidden;
}

.report-sort-field,
.report-sort-direction,
.report-measure-choice,
.report-summary-filter-field {
    min-width: 0;
    flex: 1 1 auto;
}

.report-sort-control,
.report-measure-control {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 1.75rem;
    height: 1.75rem;
    padding: 0;
    font-size: 0.75rem;
}

.report-measure-label {
    min-width: 0;
    overflow-wrap: anywhere;
}

.report-drilldown-group {
    max-width: 100%;
}

.report-summary-filter-chip {
    max-width: 100%;
}

.report-drilldown-action {
    width: 1%;
    white-space: nowrap;
}

@media (max-width: 767.98px) {
    #report-results td.report-drilldown-action {
        width: auto;
        padding-inline: 1rem !important;
        text-align: end !important;
    }

    #report-results td.report-drilldown-action::before {
        display: none;
    }
}

.report-sort-priority,
.report-measure-priority {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.125rem;
    height: 1.125rem;
    padding-inline: 0.2rem;
    border-radius: 0.25rem;
    background: var(--bs-secondary-bg);
    color: var(--bs-secondary-color);
    font-size: 0.6875rem;
    font-weight: 600;
}

/* The size select is the only bare control in the band, so it needs a width of
 * its own — auto would size it to the widest option and leave it wider than the
 * buttons beside it. */
.report-toolbar select[name="size"] {
    width: auto;
    min-width: 5rem;
}

.report-filter-chips {
    margin-top: 4px;
    background-color: var(--bs-tertiary-bg);
}

/* ---------------------------------------------------------------------------
 * REPORT FIELD PICKERS — the shape ELK Discover uses
 *
 * Two labelled sections with counts and a type glyph per row. The Columns
 * picker keeps add/remove controls visible.
 * ------------------------------------------------------------------------- */
.report-field-section + .report-field-section {
    margin-top: 0.5rem;
}

/* The summary is the fold's whole hit area, so it gets the padding and the
 * pointer. The default disclosure triangle is replaced by a chevron that turns
 * with the state — two markers would say the same thing twice, and the native
 * one cannot be styled consistently across browsers. */
.report-field-summary {
    border-bottom: 1px solid var(--bs-border-color);
    padding: 0.25rem 0;
    cursor: pointer;
    list-style: none;
    user-select: none;
}

.report-field-summary::-webkit-details-marker {
    display: none;
}

.report-field-chevron {
    width: 0.75rem;
    font-size: 0.7rem;
    color: var(--bs-secondary-color);
    transition: transform 0.12s ease-in-out;
}

details[open] > .report-field-summary .report-field-chevron {
    transform: rotate(90deg);
}

.report-columns-tabs .nav-link {
    flex: 1;
    padding: 0.5rem;
    font-size: 0.875rem;
    color: var(--bs-secondary-color);
}

.report-columns-tabs .nav-link.active {
    color: var(--bs-primary-text-emphasis);
    border-bottom-color: var(--bs-primary);
}

.report-columns-picker .report-field-summary {
    border-bottom: 0;
    margin-bottom: 0.25rem;
}

.report-columns-picker .report-field-section + .report-field-section {
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--bs-border-color);
}

.report-columns-picker .report-field-row {
    min-height: 2rem;
    padding: 0.25rem 0.375rem;
    margin-bottom: 0.125rem;
    border-radius: var(--bs-border-radius-sm);
}

.report-columns-picker .report-column-choice .report-field-glyph {
    color: var(--bs-primary-text-emphasis);
}

.report-columns-picker .report-field-action {
    opacity: 1;
}

.report-column-display-row {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-sm);
}

.report-column-display-row > .report-field-summary {
    min-height: 2.125rem;
    padding: 0.375rem 0.5rem;
    border-bottom: 0;
}

.report-column-display-row[open] {
    background-color: var(--bs-tertiary-bg);
}

.report-columns-tabs .nav-link:focus-visible,
.report-column-display-row > .report-field-summary:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
    border-radius: var(--bs-border-radius-sm);
}

/* THE ROWS ARE DENSE ON PURPOSE. Two lists stacked in one dropdown, read as
 * names rather than as controls; comfortable row spacing turns even a dozen
 * fields into a scroll. The height still clears the 24px the add button
 * occupies, so the rows do not jump when one is hovered. */
.report-field-row {
    padding: 0.0625rem 0;
    min-height: 1.5rem;
}

.report-field-glyph {
    width: 1rem;
    text-align: center;
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
    flex-shrink: 0;
}

/* REVEALED ON HOVER, NOT HIDDEN FROM KEYBOARDS. :focus-within keeps the button
 * visible while it is tabbed to — opacity alone would leave a keyboard user
 * pressing something they cannot see. */
.report-field-action {
    opacity: 0;
    transition: opacity 0.1s ease-in-out;
}

.report-field-row:hover .report-field-action,
.report-field-row:focus-within .report-field-action {
    opacity: 1;
}

.report-field-row:hover {
    background-color: var(--bs-tertiary-bg);
}

/* ICON-SIZED, NOT BUTTON-SIZED. These sit one per row in a list read as names;
 * at Bootstrap's default padding the buttons are the widest thing on the row
 * and the names stop being the subject. 1.5rem square keeps a target big
 * enough to hit while staying inside the row's own height. */
.report-field-action .btn {
    --bs-btn-padding-x: 0;
    --bs-btn-padding-y: 0;
    --bs-btn-font-size: 0.7rem;
    width: 1.5rem;
    height: 1.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
