﻿/* ==========================================================================
   Aira Helpdesk - brand overrides on top of UBold.
   Loaded after app.min.css so these win without !important.

   Only tokens and workflow-specific components belong here. Anything that is
   really a theme-wide change belongs in wwwroot/scss and a gulp rebuild -
   see .claude/docs/ubold-ui.md.
   ========================================================================== */

:root {
    /* Brand - carried over from the approved prototype. Mirrors wwwroot/scss/_aira-brand.scss,
       which is what the compiled theme derives --ins-primary from. */
    --aira-brand: #1b56a0;
    --aira-brand-600: #16467f;
    --aira-brand-400: #2a78d6;
    --aira-brand-100: #e7f0fb;
    --aira-brand-50: #f2f7fd;
    /* Ink on a brand fill - a pressed toggle, a primary button. White on the default navy; a colour
       picked in Settings gets whichever of white or #1f2937 reads better on it (BrandPalette). */
    --aira-brand-contrast: #ffffff;

    /* The sidebar. */
    --aira-nav-bg: #101a2d;
    --aira-nav-bg-2: #17233b;
    --aira-nav-ink: #aab6c8;
    --aira-nav-line: #1c2740;
    --aira-nav-muted: #5e6f8a;

    /* The rail, at the prototype's measurements. The theme ships 240 / 75; the prototype is
       258 / 74, and every inset below is derived from those two numbers.

       The one that matters is the icon: a 24px box, inside 12px of link padding, inside 12px
       of list inset - 12 + 12 + 12 = 36px, the centre of a 74px rail. That is the whole reason
       an icon does not move when the rail opens. Retune any of the three and all three have to
       be re-derived, or every icon slides sideways on hover. */
    --ins-sidenav-width: 258px;
    --ins-sidenav-width-sm: 74px;
    --ins-sidenav-item-font-size: 13.5px;
    --ins-sidenav-item-icon-size: 18px;
    --ins-sidenav-item-padding-x: 12px;
    --ins-sidenav-item-padding-y: 10px;
    --ins-sidenav-item-gap: 14px;
    --aira-sidenav-icon-box: 24px;
    --aira-sidenav-list-inset: 12px;

    /* The brand mark plus the head's padding. The scroll container is measured against this,
       not against --ins-topbar-height: the sidebar head and the topbar are two different
       boxes and the theme's own height calc conflates them. */
    --aira-sidenav-head-height: 75px;

    /* The pin row at the foot of the rail. The theme's scroll container claims the whole
       sidebar below the logo, so this height has to be subtracted from it - see below. Zero
       by default: only the two hover sizes have a footer at all. */
    --aira-sidenav-footer-height: 0px;

    /* ----------------------------------------------------------------------
       Above every dialog, however deep the stack.

       The theme puts a dialog at 1055 and a backdrop at 1050, and app/js/aira-modals.js
       raises each STACKED dialog by 20 with its backdrop between - because Bootstrap 5 gives
       every modal the same z-index and leaves the one underneath live and undimmed. The
       deepest chain the product can open is five (report -> complaints behind a figure -> one
       complaint -> a gate -> the code boxes), so the stack tops out at 1135.

       An enhanced select's panel opens FROM inside a dialog and a refused save has to be
       readable without closing one, so both have to clear the whole stack. They were at the
       theme's 1080 / 1090, which was only ever right for a dialog with nothing on top of it -
       the third dialog in a chain would have covered both. Nothing in this product renders a
       Bootstrap tooltip or popover, so 1070-1135 is unoccupied.

       A confirmation (SweetAlert2, at its own 1060) is a question about whatever is on screen,
       so it clears the stack and the panels - which are inert behind it anyway - and stays
       under the toaster, so an outcome is never hidden by the question that produced it.
       ---------------------------------------------------------------------- */
    --aira-z-panel: 1200;
    --aira-z-alert: 1205;
    --aira-z-toast: 1210;

    /* The search bar's panel (app/js/aira-search-bar.js). Above the dialog stack's 1135
       ceiling, so a bar inside the drill dialog opens over it - and BELOW the select and
       multi-select lists at 1200, because those open FROM the panel and must land on top of
       it. Any other value is either under a dialog or over its own lists. */
    --aira-z-searchpanel: 1190;

    /* The app full screen's frame (app/js/aira-fullscreen.js), over the whole host page. The top of
       the scale: in full screen the browser's top layer holds it anyway, and out of it - the frame
       kept as an overlay - nothing the host could draw should show through. */
    --aira-z-app-frame: 1220;

    /* Stage colours. One source of truth: the Kanban column heads, the stage
       chips in the ticket list and the dashboard funnel all read these, so a
       stage never appears in two different colours on two screens. */
    --stage-draft: #8b94a3;
    --stage-ccp: #eda100;
    --stage-assign: #eb6834;
    --stage-cdp: #e87ba4;
    --stage-prog: #2a78d6;
    --stage-hold: #4a3aa7;
    --stage-solved: #1baf7a;
    --stage-mktconf: #9c6ade;
    --stage-ca: #008300;
    --stage-qc: #e34948;
    --stage-closed: #56606f;

    /* The KPI strip's own palette, at the approved design's numbers.

       A tile is not always one stage - Pending is two, Solved is two, Total is every open
       stage - so it cannot simply borrow --stage-*. It is declared here rather than in C#
       because a colour in C# is a colour dark mode cannot reach: a tile spine at #008300 on
       a dark card is the same smudge a chart fill would be. */
    --kpi-draft: #8b94a3;
    --kpi-total: #2a78d6;
    --kpi-pending: #eda100;
    --kpi-prog: #eb6834;
    --kpi-hold: #4a3aa7;
    --kpi-solved: #1baf7a;
    --kpi-ca: #008300;
    --kpi-qc: #e34948;
    --kpi-closed: #56606f;
    --kpi-sla: #0ca30c;

    --kpi-cost-total: #008300;
    --kpi-cost-average: #2a78d6;
    --kpi-cost-chargeable: #eda100;
    --kpi-cost-absorbed: #e34948;
    --kpi-cost-undecided: #8b94a3;

    /* The chart palette, one hue per card, at the prototype's numbers.

       The prototype draws every bar in a card in ONE colour and reserves the eight-colour
       series list for charts whose slices are categories - a donut. Ours drew every bar in a
       different colour, which says "these seven products are seven kinds of thing" when the
       only thing that differs is the count. Tokens rather than hexes in JS for the same
       reason --kpi-* is: a hex in JS is a colour dark mode cannot reach. */
    --chart-product: #2a78d6;
    --chart-customer: #eb6834;
    --chart-received: #4a3aa7;
    --chart-division: #e87ba4;
    --chart-caction: #1baf7a;
    --chart-cost: #008300;
    --chart-trend: #2a78d6;

    /* The four departments, as a chart series (the Tickets graph, AnalysisCatalog's "dept"):
       Marketing, Customer Care, Complaint Department, Quality Control. Four hues no stage uses -
       teal, plum, brown, olive - because a department chart beside a stage chart must not read
       as the same thing, and every stage hue is already taken. */
    --dept-1: #0f9d9a;
    --dept-2: #b0328f;
    --dept-3: #8c5a3c;
    --dept-4: #6b8e23;

    /* The ageing stack and the SLA gauge. The gauge's three are the prototype's own status
       hues, which are not the same as the ageing stack's - a breach is not a stale ticket. */
    --chart-age-fresh: #1baf7a;
    --chart-age-ageing: #eda100;
    --chart-age-stale: #e34948;
    --chart-good: #0ca30c;
    --chart-warn: #fab219;
    --chart-crit: #d03b3b;

    /* Chart chrome. The theme paints axis text at --ins-secondary-color and gridlines dashed;
       the prototype's are #8b94a3 solid, with printed values a full step darker than the
       ticks so a figure reads before its axis does. */
    --chart-grid: #e9edf3;
    --chart-axis: #c9d1dd;
    --chart-tick: #8b94a3;
    --chart-dlab: #56606f;

    /* The complaint calendar's ramp - the approved design's own six steps, one per legend
       swatch, so the legend is a key a cell can be looked up in rather than a gradient the
       reader has to interpolate against the card behind it.

       The ramp is the BRAND's, which is also the prototype's: --aira-brand-400 at step 4 and
       --aira-brand at step 5, with three tints under them. It was orange for a while, which
       made the one card that is nothing but colour the one card that did not look like this
       product - and orange is already the Assign lens and the customer bars, so a busy day
       read as a stage. Hexes rather than var(--aira-brand-*): three steps are tints the brand
       scale has no token for, and stating half a ramp by reference is how it stops being
       evenly spaced.

       Six and not seven: a seventh hue introduced in a pass whose whole purpose is fidelity
       to the approved design would contradict the thing it is doing. A calendar cell carries
       a count, never a stage, so the blue is read against the legend beside it.

       Each step carries its own ink. Deciding it at runtime would need a luminance
       calculation in JS - and JS is exactly what cannot re-run here: app.js's theme observer
       repaints ApexCharts instances and nothing else, so a DOM calendar keeps whatever
       colour it was painted with, for ever, after a switch to dark mode. */
    --aira-heat-0: #eef3fb;
    --aira-heat-1: #cde2fb;
    --aira-heat-2: #9ec5f4;
    --aira-heat-3: #5598e7;
    --aira-heat-4: #2a78d6;
    --aira-heat-5: #1b56a0;
    --aira-heat-ink-0: #8b94a3;
    --aira-heat-ink-1: #17406e;
    --aira-heat-ink-2: #123659;
    --aira-heat-ink-3: #0c2a49;
    --aira-heat-ink-4: #ffffff;
    --aira-heat-ink-5: #ffffff;
    --aira-heat-ring: rgba(42, 120, 214, .45);

    /* The prototype's ink, rules and shadows, as semantic tokens.

       These are not a second copy of the --ins-* palette. They are the values UBold has no
       equivalent for, plus the two planes the board needs. --aira-ink-2 is the one that
       mattered most: the theme has body ink and one muted grey; the prototype has three -
       body ink, a *readable* secondary for card descriptions and table sub-text, and a
       genuinely muted grey. Collapsing the middle one onto --ins-secondary-color is what
       made every board card read as disabled. */
    --aira-ink: #131a26;
    --aira-ink-2: #56606f;
    --aira-muted: #8b94a3;
    --aira-line: #e5e9f0;
    --aira-line-2: #d6dce6;
    --aira-surface-2: #f8fafc;

    /* The board's ground. It has to be a clear step darker than the page, or the white cards
       on it have nothing to sit on and the whole board reads as one flat sheet. */
    --aira-board-bg: #eef1f6;

    /* Crisp and close. UBold's card shadow is `0 12px 96px rgba(56,65,74,.03)` - a 96px blur
       at 3% opacity, which is not a shadow, it is a rumour. */
    --aira-shadow-1: 0 1px 2px rgba(19, 26, 38, .06), 0 1px 3px rgba(19, 26, 38, .04);
    --aira-shadow-2: 0 4px 16px rgba(19, 26, 38, .10), 0 1px 3px rgba(19, 26, 38, .06);
}

/* The same set in dark mode. A hard-coded #eef1f6 board ground would be a white slab in the
   middle of a dark page, and every card on it unreadable. */
html[data-bs-theme="dark"] {
    --aira-ink: #e4e8ee;
    --aira-ink-2: #aab8c5;
    --aira-muted: #8391a2;
    --aira-line: #293036;
    --aira-line-2: #333b42;
    --aira-surface-2: #1e1f27;
    --aira-board-bg: #1b1c24;
    --aira-shadow-1: 0 1px 2px rgba(0, 0, 0, .35);
    --aira-shadow-2: 0 4px 16px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .3);

    /* The stage palette, lifted for dark mode.

       The chips got away without this: they render their stage colour at 12% alpha through
       color-mix, so any hex survives behind them. A CHART does not - it fills a whole donut
       ring or column with the flat colour, and #008300 and #56606f on a #1b1c24 card are two
       barely-distinguishable dark smudges. These are the same hues lifted in luminance until
       they read as themselves against a dark ground, which keeps the board, the chip and the
       chart agreeing about which stage is which without any of them being unreadable. */
    --stage-draft: #a5adba;
    --stage-ccp: #f5b731;
    --stage-assign: #ff8355;
    --stage-cdp: #f294b6;
    --stage-prog: #529ff0;
    --stage-hold: #7d6ce0;
    --stage-solved: #2fd39a;
    --stage-mktconf: #b58bf0;
    --stage-ca: #21ab53;
    --stage-qc: #ff6f6e;
    --stage-closed: #8c98a8;

    /* The KPI palette lifted the same way, and for the same reason: the spine is a solid
       3px bar, so the hue has to carry at full saturation against a dark card. */
    --kpi-draft: #a5adba;
    --kpi-total: #529ff0;
    --kpi-pending: #f5b731;
    --kpi-prog: #ff8355;
    --kpi-hold: #7d6ce0;
    --kpi-solved: #2fd39a;
    --kpi-ca: #21ab53;
    --kpi-qc: #ff6f6e;
    --kpi-closed: #8c98a8;
    --kpi-sla: #2ecc57;

    --kpi-cost-total: #21ab53;
    --kpi-cost-average: #529ff0;
    --kpi-cost-chargeable: #f5b731;
    --kpi-cost-absorbed: #ff6f6e;
    --kpi-cost-undecided: #a5adba;

    /* The chart palette lifted the same way. A bar is a solid fill, so the hue has to carry
       at full saturation against a dark card - #008300 on #1b1c24 is a smudge. */
    --chart-product: #529ff0;
    --chart-customer: #ff8355;
    --chart-received: #8e7ef0;
    --chart-division: #f294b6;
    --chart-caction: #2fd39a;
    --chart-cost: #21ab53;
    --chart-trend: #529ff0;

    --dept-1: #2cc4c0;
    --dept-2: #d95cb6;
    --dept-3: #b98363;
    --dept-4: #97bd4a;

    --chart-age-fresh: #2fd39a;
    --chart-age-ageing: #f5b731;
    --chart-age-stale: #ff6f6e;
    --chart-good: #2ecc57;
    --chart-warn: #f5b731;
    --chart-crit: #ff6f6e;

    --chart-grid: #2b313a;
    --chart-axis: #3a424c;
    --chart-tick: #8391a2;
    --chart-dlab: #aab8c5;

    /* The ramp inverted in luminance, not faded in opacity. An opacity ramp on a dark card
       collapses towards the ground under it and the quiet days stop being readable at all,
       which is the half of the calendar that carries "nothing much happened". The top two
       steps are the lightest ones here, so theirs are the dark inks. */
    --aira-heat-0: #232630;
    --aira-heat-1: #16324f;
    --aira-heat-2: #1e4d80;
    --aira-heat-3: #2a6fbf;
    --aira-heat-4: #4e97ea;
    --aira-heat-5: #8dbcf5;
    --aira-heat-ink-0: #6b7684;
    --aira-heat-ink-1: #9cc0e8;
    --aira-heat-ink-2: #d3e4f8;
    --aira-heat-ink-3: #ffffff;
    --aira-heat-ink-4: #0a2743;
    --aira-heat-ink-5: #0a2743;
    --aira-heat-ring: rgba(141, 188, 245, .5);
}

/* --------------------------------------------------------------------------
   The light palette, corrected to the approved design.

   UBold's light theme is a full stop paler than the prototype: body text at #4c4c5c, muted at
   #9ba6b7, `--ins-emphasis-color` at 75% opacity - *lighter* than the body colour it is
   supposed to emphasise - and the card shadow above. On screen that reads as a greyed-out
   build rather than a calm one; headings look disabled and nothing separates a card from the
   panel under it.

   Scoped to `html:not([data-bs-theme="dark"])` on purpose, and this is the trap: the theme
   declares its light values on bare `:root` and its dark ones on `[data-bs-theme="dark"]`,
   both (0,1,0). aira.css loads last, so a plain `:root` block here would also outrank the
   DARK block and repaint dark mode in light ink. The :not() takes it to (0,1,1), which beats
   the light declarations and never applies in dark mode.
   -------------------------------------------------------------------------- */
html:not([data-bs-theme="dark"]) {
    --ins-body-color: var(--aira-ink);
    --ins-body-color-rgb: 19, 26, 38;
    --ins-emphasis-color: var(--aira-ink);
    --ins-emphasis-color-rgb: 19, 26, 38;

    --ins-secondary-color: var(--aira-muted);
    --ins-secondary-color-rgb: 139, 148, 163;

    --ins-body-bg: #f3f5f9;
    --ins-body-bg-rgb: 243, 245, 249;
    --ins-border-color: var(--aira-line);

    /* Also --ins-theme-card-box-shadow, so every .card in the product gains an edge. */
    --ins-box-shadow-sm: var(--aira-shadow-1);
}

/* UBold renames Bootstrap's CSS-variable prefix from `bs` to `ins`: the theme defines
   --ins-primary, --ins-border-color and so on, and defines no --bs-* variable at all. The
   [data-bs-theme] *selector* is unchanged, which is what makes the rename easy to miss -
   var(--bs-success) simply resolves to nothing and the colour silently falls back.
   Everything below reads --ins-*. See .claude/docs/ubold-ui.md.

   The brand itself is NOT set here. --ins-primary is compiled from $aira-brand in
   wwwroot/scss/_aira-brand.scss, so every button, link, focus ring, badge and table accent
   derives from one value rather than being patched component by component. Restating it
   here would give the brand two sources of truth. */

/* --------------------------------------------------------------------------
   Keyboard focus - which the product had none of, anywhere.

   THE THEME ERASES IT TWICE OVER, and neither one is visible from the source:

     * `a, button { outline: none !important }` - one blanket rule in app.min.css that takes
       the ring off every link and every button there is. It carries `!important`, so it also
       beat the rings THIS file already declared: the timeline's own
       `.tl-filter:focus-visible { outline: 2px solid var(--ins-primary) }` matched, lost, and
       has painted nothing for as long as it has existed.

     * `$btn-focus-width`, `$form-select-focus-width` and `$form-check-focus-width` are all
       compiled to zero, so `--ins-btn-focus-box-shadow` is `0 0 0 0 rgba(...)` - a ring with
       no spread. The rule fires and draws nothing. `--ins-btn-close-focus-shadow` and
       `--ins-accordion-btn-focus-box-shadow` are outright `none`, `.nav-link:focus-visible`
       is another zero, and SweetAlert2's own ring dies on `.swal2-styled:focus { box-shadow:
       none }`.

   The net of it was that a `.form-control` was the ONLY control in the product that said
   anything when the keyboard reached it. Every button, every link, every select, checkbox,
   switch and tab - and both buttons of "Discard your changes?" - took focus, held the
   keyboard and answered Enter with nothing on screen saying which one was live. Read out of
   Chrome with CSS.getMatchedStylesForNode rather than off the source, because a rule that
   matches and draws nothing looks exactly like a rule that works.

   So: one ring, declared once, for everything that takes focus. The `!important` on the
   outline is not a shortcut - it is the only thing that beats an `!important` in a file
   rule 10 forbids editing.

   TWO LANGUAGES, on purpose, and both are ones the theme already speaks:

     * a FIELD glows - the brand border and the soft halo `.form-control:focus` has always
       drawn. Selects, checkboxes and switches simply never did; now they match the text box
       beside them instead of growing a second look for the same state.
     * an ACTION is ringed - 2px of the brand, held 2px off the control so the gap shows the
       ground behind it and the ring reads against a filled button as well as a bare one.

   Tuning it: set `--aira-focus-ring-offset` on the component (negative to sit the ring
   inside, which is what a row reaching its container's edge needs). Never restate the
   `outline` - it loses to the `!important` above, which is the whole point of it.
   -------------------------------------------------------------------------- */

:root {
    --aira-focus-ring-width: 2px;
    --aira-focus-ring-offset: 2px;
    --aira-focus-ring-color: var(--ins-primary);
}

/* The brand does not move in dark mode - BrandPalette keeps --ins-primary and lifts the
   readable derivations around it - so a navy ring on a near-black panel is a ring nobody
   sees. --ins-primary-text-emphasis is the theme's own "this brand, against this ground",
   and it follows the colour picked under Settings in both modes. */
html[data-bs-theme="dark"] {
    --aira-focus-ring-color: var(--ins-primary-text-emphasis);
}

/* Windows High Contrast throws every author colour away, the ring's included. Highlight is
   the system's own focus colour and is the one value guaranteed to contrast with whatever it
   replaced them with. */
@media (forced-colors: active) {
    :root {
        --aira-focus-ring-color: Highlight;
    }
}

/* Where the ring sits, at the lowest specificity there is, so a component that needs it
   somewhere else wins with one declaration rather than having to out-shout the rule below.
   `outline` is a shorthand for colour, style and width only - it does not reset the offset. */
:focus-visible {
    outline-offset: var(--aira-focus-ring-offset);
}

/* An action: everything that takes focus and is not a field. The two enhanced selects are
   `<button class="form-select">` and are excluded here on purpose - they are fields, they
   stand where a native select stood, and they glow with the rest of the form below. */
a:focus-visible,
button:not(.form-select):not(.form-control):focus-visible,
[role="button"]:focus-visible,
[role="tab"]:focus-visible,
summary:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible,
.btn-check:focus-visible + .btn,
/* A checkbox, a radio and a switch are 1em square. The border going navy carries the state on
   a 45px field and does not carry it here - there is not enough of the control to see it change
   - so the small controls take the ring as well as the glow. Keyboard only: a click on a
   checkbox is aimed, and the box it lands in does not need pointing at. */
.form-check-input:focus-visible {
    outline: var(--aira-focus-ring-width) solid var(--aira-focus-ring-color) !important;
}

/* The shell's dark surfaces. Everywhere else the brand rings against a near-white page at
   6.7:1 or, in dark mode, at 5.6:1 - comfortably past the 3:1 a focus indicator owes its
   surroundings. The rail is the exception, and twice over: navy on the rail's own ground is
   2.4:1, and the ACTIVE item's pill is the brand itself, so the ring would be the same colour
   as the thing it rings. White is 17:1 on the rail and 7.3:1 on that pill. Measured, not
   judged by eye - a dim ring looks like a design choice in a screenshot.

   The rail is dark in both colour modes (`data-menu-color="dark"` from _Layout), so this
   follows the attribute rather than the theme. */
html[data-menu-color="dark"] .sidenav-menu,
html[data-topbar-color="dark"] .topbar {
    --aira-focus-ring-color: #ffffff;
}

/* Rows that reach their container's edge. A ring held outside one is a ring the menu's own
   rounding and overflow clip away, so these wear it inside. */
.dropdown-item,
.list-group-item-action,
.aira-bell-item,
.aira-sel-opt,
.aira-ms-opt {
    --aira-focus-ring-offset: -2px;
}

/* A field. Restated at the theme's own specificity, which is all it takes - aira.css loads
   last. Only the two compiled-to-zero rings are touched; `.form-control:focus` already draws
   exactly this and is left alone, so the three controls finally agree. */
.form-select:focus,
.form-check-input:focus {
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 3px var(--ins-primary-bg-subtle);
}

/* The question over everything - Aira.confirm, and every Swal.fire behind it.

   Its buttons ring on plain `:focus`, and they are the only controls in the product that do.
   A question is answered with Enter, so which button Enter presses has to be on screen from
   the moment it opens - and SweetAlert2 puts focus on that button ITSELF, programmatically,
   which is precisely the move Chrome decides is not `:focus-visible`. Waiting for
   `:focus-visible` means the default answer is invisible until the operator has already
   pressed a key to go looking for it.

   They can afford it: the question is gone the moment either button is pressed, so a ring
   left behind by a mouse click has nothing to linger on.

   (0,3,0) against the theme's `.swal2-styled:focus { box-shadow: none }` at (0,2,0). The
   `!important` is for the same `a, button` rule as everywhere else. */
.swal2-actions .swal2-styled:focus,
.swal2-popup .swal2-close:focus-visible {
    outline: var(--aira-focus-ring-width) solid var(--aira-focus-ring-color) !important;
    outline-offset: var(--aira-focus-ring-offset);
}

/* --------------------------------------------------------------------------
   Refused controls - Aira.refuse in app/js/aira.js (ubold-ui.md, *Refused controls*).

   The theme's disabled look without the theme's `pointer-events: none`, which took the cursor,
   the hover and the reason's tooltip with it - so a refusal whose sentence the code had written
   carefully showed nobody anything. The control keeps its focus, so the ring above still marks
   it and a keyboard user learns why too.
   -------------------------------------------------------------------------- */
.is-refused[aria-disabled="true"] { cursor: not-allowed; }

.btn.is-refused[aria-disabled="true"],
.btn.is-refused[aria-disabled="true"]:hover {
    color: var(--ins-btn-disabled-color);
    background-color: var(--ins-btn-disabled-bg);
    border-color: var(--ins-btn-disabled-border-color);
    opacity: var(--ins-btn-disabled-opacity);
    box-shadow: none;
}

.dropdown-item.is-refused[aria-disabled="true"],
.dropdown-item.is-refused[aria-disabled="true"]:hover {
    color: var(--ins-dropdown-link-disabled-color);
    background-color: transparent;
}

/* The sentence under the pointer, with the ban sign before it; wider than Bootstrap's 200px so a
   reason reads as a line rather than a column. */
.aira-refuse-tooltip { --ins-tooltip-max-width: 18rem; }
.aira-refuse-tip { display: inline-flex; align-items: flex-start; gap: .375rem; text-align: left; }
.aira-refuse-icon { flex: 0 0 auto; margin-top: .2em; }

/* --------------------------------------------------------------------------
   Toggles - every segmented control: the Tickets page's view switch, the Graph's types, modes and
   order, the Pivot's Heat, the inbox's All / Unread, the audit log's view (ubold-ui.md, *Toggles*).

   Pressed is the brand, filled, with ink that reads on it whatever colour Settings holds
   (--aira-brand-contrast, BrandPalette). It supersedes T42's outline-secondary, whose pressed
   grey-blue was not the software's colour, and keeps T42's reason: pressed reads MORE pressed than
   rest, never paler. aria-pressed is the only state - no .active, which the theme paints on its own.
   Toolbar controls, so they carry -sm (hard rule 10a); the focus ring is the central one (10c).
   -------------------------------------------------------------------------- */
.aira-toggle {
    --ins-btn-color: var(--ins-body-color);
    --ins-btn-bg: transparent;
    --ins-btn-border-color: var(--ins-border-color);
    --ins-btn-hover-color: var(--ins-emphasis-color);
    --ins-btn-hover-bg: color-mix(in srgb, var(--aira-brand-400) 9%, transparent);
    --ins-btn-hover-border-color: var(--ins-border-color);
    --ins-btn-active-color: var(--ins-emphasis-color);
    --ins-btn-active-bg: color-mix(in srgb, var(--aira-brand-400) 16%, transparent);
    --ins-btn-active-border-color: var(--ins-border-color);
    --ins-btn-disabled-color: var(--ins-body-color);
    --ins-btn-disabled-bg: transparent;
    --ins-btn-disabled-border-color: var(--ins-border-color);
}

/* Variables only: the theme's .btn, :hover, :active and :focus-visible read these, so every state
   of a pressed toggle is the brand without restating a property. */
.aira-toggle[aria-pressed="true"] {
    --ins-btn-color: var(--aira-brand-contrast);
    --ins-btn-bg: var(--ins-primary);
    --ins-btn-border-color: var(--ins-primary);
    --ins-btn-hover-color: var(--aira-brand-contrast);
    --ins-btn-hover-bg: var(--aira-brand-600);
    --ins-btn-hover-border-color: var(--aira-brand-600);
    --ins-btn-active-color: var(--aira-brand-contrast);
    --ins-btn-active-bg: var(--aira-brand-600);
    --ins-btn-active-border-color: var(--aira-brand-600);
    --ins-btn-disabled-color: var(--aira-brand-contrast);
    --ins-btn-disabled-bg: var(--ins-primary);
    --ins-btn-disabled-border-color: var(--ins-primary);
}

/* --------------------------------------------------------------------------
   The sidebar, in the prototype's navy. These are only custom properties, so
   they belong here rather than in the SCSS - no rebuild needed to retune them.
   `data-menu-color="dark"` is set on <html> by _Layout.
   -------------------------------------------------------------------------- */
html[data-menu-color="dark"],
/* The theme collapses dark, gray and light into one shared block in dark colour mode:
   `html[data-bs-theme="dark"][data-menu-color="dark"]`. That selector outranks a plain
   `html[data-menu-color="dark"]` however late this file loads, so the rail silently stops
   being navy the moment somebody switches to dark mode. Stated at both specificities.
   See .claude/docs/ubold-layout.md. */
html[data-bs-theme="dark"][data-menu-color="dark"] {
    --ins-sidenav-bg: var(--aira-nav-bg);
    --ins-sidenav-border-color: var(--aira-nav-bg);
    --ins-sidenav-item-color: var(--aira-nav-ink);
    --ins-sidenav-item-hover-color: #ffffff;
    --ins-sidenav-item-hover-bg: var(--aira-nav-bg-2);
    --ins-sidenav-item-active-color: #ffffff;
    /* The active item is the brand, not a darker shade of the bar - it is the only
       thing on the sidebar that has to be findable at a glance. */
    --ins-sidenav-item-active-bg: var(--aira-brand);
}

html[data-menu-color="dark"] .side-nav-title {
    color: #4f5f79;
}

/* The prototype's section labels are wider-tracked than the theme's, and sit tighter to the
   group they head than the theme's symmetrical 10px. */
.side-nav .side-nav-title {
    letter-spacing: .18em;
    padding: 16px 24px 7px;
    /* Tracked for the open rail, so it is far wider than 74px. Without its own clip the
       overflow reaches SimpleBar's scrollWidth and the collapsed menu becomes scrollable
       sideways - invisibly, because the theme paints that track transparent. */
    overflow: hidden;
}

/* The prototype weights the current page. Matches the theme's own
   `.side-nav > .side-nav-item.active > a`, which is where the class has to live. */
html[data-menu-color="dark"] .side-nav > .side-nav-item.active > .side-nav-link {
    font-weight: 600;
}

html[data-menu-color="dark"] .side-nav-link .badge {
    background: #2a3a57 !important;
    color: #c3d0e4 !important;
}

html[data-menu-color="dark"] .side-nav-item.active .side-nav-link .badge {
    background: rgba(255, 255, 255, .22) !important;
    color: #fff !important;
}

/* --------------------------------------------------------------------------
   The rail's geometry - so nothing moves when it opens.

   The theme's on-hover rail centres each icon while collapsed and left-aligns it once
   expanded, scales it up 15% on the way, and drops the labels, counts and section titles
   out of the flow with `display: none`. Every icon therefore slides and every row jumps the
   moment the pointer arrives. The prototype does none of that: one fixed layout at both
   widths, and only the opacity of the text changes.

   Everything below restates a theme rule at its own specificity or higher. aira.css loads
   after app.min.css, so an equal-specificity restatement wins - the counts in the comments
   are what make each one safe to move. See .claude/docs/ubold-layout.md.
   -------------------------------------------------------------------------- */
.sidenav-menu {
    transition: width .18s cubic-bezier(.4, 0, .2, 1);
}

/* Labels are laid out at their full open width even while the rail is 74px, so the rail itself
   has to be the clip. Only these two sizes: condensed flies a sub-menu out past the rail's
   right edge, and a blanket clip would swallow it the day the menu grows one. */
html[data-sidenav-size="on-hover"] .sidenav-menu,
html[data-sidenav-size="on-hover-active"] .sidenav-menu {
    overflow: hidden;
}

/* Hovering floats the rail over the page; pinning makes it part of the page. */
html[data-sidenav-size="on-hover"] .sidenav-menu:hover {
    box-shadow: 0 18px 50px rgba(19, 26, 38, .2);
}

.side-nav {
    gap: 2px;
    padding-top: 6px;
    /* The theme reserves 50px for its floating pin button. Ours is a real footer row. */
    padding-bottom: 16px;
}

.side-nav > .side-nav-item {
    padding: 0 var(--aira-sidenav-list-inset);
}

.side-nav .side-nav-item .side-nav-link {
    border-radius: 9px;
    /* A label typeset for the open rail still exists at 74px. Without this it escapes the row
       and SimpleBar grows a horizontal scrollbar under the menu. */
    overflow: hidden;
}

/* The fixed box is what holds the icon at one x-position in both states. */
.side-nav .side-nav-item .side-nav-link .menu-icon {
    flex: 0 0 var(--aira-sidenav-icon-box);
    width: var(--aira-sidenav-icon-box);
    display: flex;
    justify-content: center;
}

/* (0,6,1), over the theme's (0,5,1) `:not(:hover)` centring - the sideways slide. */
html[data-sidenav-size="on-hover"] .sidenav-menu .side-nav .side-nav-item .side-nav-link,
html[data-sidenav-size="on-hover-active"] .sidenav-menu .side-nav .side-nav-item .side-nav-link {
    justify-content: flex-start;
}

/* The theme also grows the collapsed icon to 115%, which is a second, subtler jump. */
html[data-sidenav-size="on-hover"] .sidenav-menu .side-nav .side-nav-item .side-nav-link .menu-icon,
html[data-sidenav-size="on-hover"] .sidenav-menu .side-nav .side-nav-item .side-nav-link .menu-icon i {
    font-size: var(--ins-sidenav-item-icon-size);
    line-height: var(--ins-sidenav-item-icon-size);
}

html[data-sidenav-size="on-hover"] .sidenav-menu .side-nav .side-nav-item .side-nav-link .menu-icon svg {
    width: var(--ins-sidenav-item-icon-size);
    height: var(--ins-sidenav-item-icon-size);
}

/* Labels, counts and section titles keep their place in the flow at both widths - taking a
   section title out of the flow is what shunts the whole menu upwards while collapsed. They
   are faded instead. Stated unconditionally, so there is no state in which the theme's
   `display: none` can come back; the fade below is the only thing that hides them. */
html[data-sidenav-size="on-hover"] .sidenav-menu .scrollbar .side-nav .side-nav-item .side-nav-link .menu-text {
    display: block;
}

html[data-sidenav-size="on-hover"] .sidenav-menu .scrollbar .side-nav .side-nav-item .side-nav-link .badge {
    display: inline-block;
}

html[data-sidenav-size="on-hover"] .sidenav-menu .scrollbar .side-nav .side-nav-title {
    display: block;
}

.sidenav-menu .menu-text,
.sidenav-menu .side-nav-title,
.sidenav-menu .side-nav-link .badge {
    transition: opacity .15s;
}

/* Keyboard focus opens the rail exactly as hover does. Without it, tabbing through the menu
   moves focus between rows whose labels the person cannot read. (0,3,1) ties the theme's
   `:not(:hover)` width rule and wins on order. */
html[data-sidenav-size="on-hover"] .sidenav-menu:focus-within {
    width: var(--ins-sidenav-width);
}

html[data-sidenav-size="on-hover"] .sidenav-menu:not(:hover):not(:focus-within) .scrollbar .side-nav .side-nav-item .side-nav-link .menu-text,
html[data-sidenav-size="on-hover"] .sidenav-menu:not(:hover):not(:focus-within) .scrollbar .side-nav .side-nav-item .side-nav-link .badge,
html[data-sidenav-size="on-hover"] .sidenav-menu:not(:hover):not(:focus-within) .scrollbar .side-nav .side-nav-title,
html[data-sidenav-size="on-hover"] .sidenav-menu:not(:hover):not(:focus-within) .sidenav-footer .menu-text {
    opacity: 0;
}

/* --------------------------------------------------------------------------
   The brand mark - the prototype's gradient tile with the software's initials,
   replaced by the uploaded logo when Settings has one. One partial, two places.
   -------------------------------------------------------------------------- */
.aira-brand {
    display: flex;
    align-items: center;
    gap: 13px;
    min-width: 0;
}

.aira-brand-mark {
    flex: 0 0 42px;
    width: 42px;
    height: 42px;
    border-radius: 11px;
    overflow: hidden;
    display: grid;
    place-items: center;
    background: linear-gradient(150deg, var(--aira-brand-400), var(--aira-brand));
    color: #fff;
    font-weight: 800;
    font-size: 1rem;
    letter-spacing: -.02em;
}

.aira-brand-mark img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* The initials sit under the logo and only show when there is no logo, or when one was
   configured and the file has since gone missing. */
.aira-brand-mark:has(img) .aira-brand-initials { display: none; }
.aira-brand-mark.is-fallback .aira-brand-initials { display: block; }

.aira-brand-text {
    min-width: 0;
    line-height: 1.25;
}

.aira-brand-text b {
    display: block;
    color: #fff;
    font-size: .875rem;
    font-weight: 700;
    letter-spacing: .08em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Sized to fit "Aira Euro Automation" in the 258px rail. A longer company name still
   ellipses rather than pushing the mark - an operator can type anything into Settings. */
.aira-brand-text span {
    display: block;
    color: #6d7e97;
    font-size: .625rem;
    letter-spacing: .2em;
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The collapsed rail is only wide enough for the mark. The theme fades the menu labels on
   the same transition, so the wordmark follows them rather than clipping mid-letter. */
html[data-sidenav-size="on-hover"] .sidenav-menu .aira-brand-text {
    opacity: 0;
    transition: opacity .15s;
}

html[data-sidenav-size="on-hover"] .sidenav-menu:hover .aira-brand-text,
html[data-sidenav-size="on-hover"] .sidenav-menu:focus-within .aira-brand-text,
html[data-sidenav-size="on-hover-active"] .sidenav-menu .aira-brand-text {
    opacity: 1;
}

.aira-brand-link,
.aira-brand-link:hover {
    text-decoration: none;
}

/* The theme's .logo box is built around a single centred image on a 70px line-height. Ours
   is a two-part flex row, so it needs its own box: the prototype's head with real padding,
   rather than a mark jammed against the top corner.

   16px of padding either side of a 42px mark centres it in the 74px rail, which is what keeps
   the mark still while the rail opens - the same arithmetic as the menu icons above. */
.sidenav-menu .logo {
    display: flex;
    align-items: center;
    min-height: var(--aira-sidenav-head-height);
    padding: 17px 16px 16px;
    line-height: 1.25;
    text-align: left;
}

/* Condensed zeroes this padding at (0,3,1) and would drop the mark into the top-left corner.
   Restated at the same specificity so the rail below 1140px is laid out like the one above it. */
html[data-sidenav-size="condensed"] .sidenav-menu .logo {
    padding: 17px 16px 16px;
}

/* Only in offcanvas does anything share this box - the close button, absolutely positioned
   over its right edge. Elsewhere the wordmark gets the full width. */
html[data-sidenav-size="offcanvas"] .sidenav-menu .logo {
    padding-right: 40px;
}

/* --------------------------------------------------------------------------
   The pin, at the foot of the rail - the prototype's "Pin sidebar open".

   The theme ships this as `.button-on-hover`, a bare icon absolutely positioned
   in the logo corner. The class is kept so app.js still owns the toggle; only
   the box it sits in is ours. See .claude/docs/ubold-layout.md.
   -------------------------------------------------------------------------- */
html[data-sidenav-size="on-hover"],
html[data-sidenav-size="on-hover-active"] {
    --aira-sidenav-footer-height: 60px;
}

/* The theme gives the scroll container the entire rail below the logo, so the footer has to
   be carved out of it or the pin lands on top of the last menu item. The head is measured by
   its own token: the theme subtracts --ins-topbar-height here, which is a different box. */
.sidenav-menu [data-simplebar] {
    height: calc(100% - var(--aira-sidenav-head-height) - var(--aira-sidenav-footer-height));
}

.sidenav-footer {
    display: none;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--aira-sidenav-footer-height);
    align-items: center;
    padding: 10px var(--aira-sidenav-list-inset) 14px;
    border-top: 1px solid var(--aira-nav-line);
    background: var(--ins-sidenav-bg);
}

/* Condensed, compact and offcanvas have no hover state to pin, so the row would be a button
   that does nothing you can see. */
html[data-sidenav-size="on-hover"] .sidenav-footer,
html[data-sidenav-size="on-hover-active"] .sidenav-footer {
    display: flex;
}

.sidenav-footer .button-on-hover {
    position: static;
    align-items: center;
    gap: var(--ins-sidenav-item-gap);
    width: 100%;
    padding: 9px var(--ins-sidenav-item-padding-x);
    border-radius: 9px;
    line-height: 1.25;
    font-size: .78125rem;
    white-space: nowrap;
    color: var(--aira-nav-muted);
    transition: background-color .13s, color .13s;
}

.sidenav-footer .button-on-hover:hover,
.sidenav-footer .button-on-hover:focus-visible {
    background: var(--ins-sidenav-item-hover-bg);
    color: var(--ins-sidenav-item-hover-color);
}

/* The theme reveals this button with `display: block` at (0,3,1), which beats a plain
   `.sidenav-footer .button-on-hover` - and a block box lays the icon and label out as running
   text. The row has to restate flex at the theme's own specificity. */
html[data-sidenav-size="on-hover"] .sidenav-menu .sidenav-footer .button-on-hover,
html[data-sidenav-size="on-hover-active"] .sidenav-menu .sidenav-footer .button-on-hover {
    display: flex;
}

/* Lucide renders a 24px SVG by default. Inside .side-nav the theme sizes it; the footer is
   outside that, so it needs a size stated or the pin dwarfs every menu icon. The prototype
   draws the pin a notch smaller than the menu icons - the shared 24px box below is what still
   holds it on their x-axis.

   The pin carries .menu-icon too, so the theme's collapsed 115% scale-up reaches it at (0,4,1);
   (0,5,1) here is what stops the pin growing and shrinking as the rail opens. */
html[data-sidenav-size="on-hover"] .sidenav-menu .sidenav-footer .aira-pin-icon svg,
html[data-sidenav-size="on-hover-active"] .sidenav-menu .sidenav-footer .aira-pin-icon svg {
    width: 16px;
    height: 16px;
}

/* Pinned reads as a state, not a hover. The theme colours it --ins-primary, which is the
   brand navy sitting on the navy rail - invisible. The prototype's lighter blue instead;
   (0,5,1) to clear the theme's own (0,4,1) rule. */
html[data-sidenav-size="on-hover-active"] .sidenav-menu .sidenav-footer .button-on-hover {
    color: var(--aira-brand-400);
}

/* The theme hides the whole button on the collapsed rail. The prototype keeps the icon and
   fades only the label, which is what makes the pin discoverable before you hover. The row is
   NOT re-centred here: that was the pin's own copy of the icon jump the menu no longer has.
   Has to out-specify `html[…on-hover] .sidenav-menu:not(:hover) .button-on-hover`. */
html[data-sidenav-size="on-hover"] .sidenav-menu:not(:hover) .sidenav-footer .button-on-hover {
    display: flex;
}

/* The theme drops every .menu-text out of the collapsed rail at (0,4,1). The pin's label is
   faded along with the menu's instead. Only the "off" label: the "on" one belongs to the
   pinned state and is switched by the rules below. */
html[data-sidenav-size="on-hover"] .sidenav-menu .sidenav-footer .menu-text.aira-pin-off {
    display: inline;
}

/* Two icons and two labels, swapped by state rather than rewritten in script: Lucide replaces
   each <i> with an <svg> at load, so a JS swap would have to re-run createIcons every click. */
.sidenav-footer .aira-pin-icon {
    display: inline-flex;
    justify-content: center;
    flex: 0 0 var(--aira-sidenav-icon-box);
}

.sidenav-footer .aira-pin-on {
    display: none;
}

html[data-sidenav-size="on-hover-active"] .sidenav-footer .aira-pin-off {
    display: none;
}

html[data-sidenav-size="on-hover-active"] .sidenav-footer .aira-pin-icon.aira-pin-on {
    display: inline-flex;
}

html[data-sidenav-size="on-hover-active"] .sidenav-footer .aira-pin-label.aira-pin-on {
    display: inline;
}

/* The topbar carries the mark alone - the wordmark is already in the sidebar, and the page
   title sits immediately to its right. */
.logo-topbar {
    line-height: 1.25;
}

.logo-topbar .aira-brand-mark {
    flex-basis: 32px;
    width: 32px;
    height: 32px;
    border-radius: 8px;
    font-size: .8rem;
}

/* --------------------------------------------------------------------------
   The page title, on the topbar.

   `_PageTitle` is rendered by `_Topbar`, not by the page - the prototype puts the screen's
   name and its one-line description immediately left of the search. The band it used to
   occupy at the top of `.container-fluid` is what pushed the Kanban columns below the fold.
   -------------------------------------------------------------------------- */
/* Which group gives up width, in order.

   `.topbar-menu` sizes both groups to their content. The right-hand one is icons and a name and
   has no slack: squeezing it wrapped "Mitul A Modi" onto three lines at 1200px, and clipped it
   entirely at 1024. So it never shrinks, and the left group - title, then search - absorbs the
   whole shortfall by ellipsising and narrowing. */
.aira-topbar-left {
    min-width: 0;
    /* Grows, so the search actually gets its 380px basis. With `flex: 0 1 auto` the group is
       sized from its children's *max-content* rather than their flex bases, which quietly gave
       the search 287px and ellipsised the subtitle on a 1600px screen with room to spare.
       Growing cannot push the toolbar off the edge the way a growing search would: the toolbar
       is a sibling of this group, not a child of it, and it does not shrink. */
    flex: 1 1 auto;
}

.aira-topbar-right {
    flex: 0 0 auto;
}

.aira-topbar-right .nav-user h5 {
    white-space: nowrap;
}

.aira-page-title {
    /* min-width:0 is what lets the ellipsis below actually fire: a flex item defaults to
       min-width:auto and refuses to shrink under its text, which would instead push the
       search - and then the toolbar - off the right edge. */
    min-width: 0;
    flex: 0 1 auto;
    max-width: 20rem;
    margin-left: .375rem;
    padding-right: .5rem;
}

.aira-page-title-h {
    margin: 0;
    font-size: 1.03125rem;
    font-weight: 650;
    letter-spacing: -.01em;
    line-height: 1.25;
    color: var(--aira-ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The title used to supply the page's top margin through `.page-title-head`'s 1.5rem of
   padding. With it gone the first card butts against the topbar, so the gutter is stated
   here instead - on the content wrapper, where it applies to every screen. */
.content-page > .container-fluid {
    padding-top: 1.25rem;
}

/* The prototype's search: a fixed lane next to the logo rather than a bar that grows to fill
   the topbar. The right-hand toolbar is the topbar's second flex child - see _Topbar.cshtml -
   so the search must NOT take flex-grow, or it pushes the toolbar off the right edge. */
.aira-topbar-search {
    /* Shrinks, never grows. Growing would push the right-hand group off the edge; refusing to
       shrink would do the same to the page title beside it. */
    flex: 0 1 380px;
    min-width: 12rem;
}

.aira-topbar-search .app-search,
.aira-topbar-search .app-search .position-relative {
    width: 100%;
}

/* --------------------------------------------------------------------------
   Search fields - the topbar's global search and the Configuration rail's filter.
   -------------------------------------------------------------------------- */

/* The prototype's search is a pill on a tinted ground, not the theme's 4.8px rounded box.
   Scoped to .app-search - the magnifier-in-the-field pattern - so a search input that is one
   half of an .input-group keeps its square edge against the button beside it. */
.app-search .form-control {
    border-radius: 50rem;
    background-color: var(--ins-tertiary-bg);
}

/* The theme flattens :focus to no ring at all. The prototype lifts the field to the plain
   surface and rings it in the brand. Both colours are derived, so the primary colour an
   operator picks under Settings carries into it. */
.app-search .form-control:focus {
    background-color: var(--ins-secondary-bg);
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 3px var(--ins-primary-bg-subtle);
}

/* SimpleBar paints its own track; without this it is a light bar on a navy panel. */
html[data-menu-color="dark"] .sidenav-menu .simplebar-scrollbar::before {
    background: #26334d;
}

/* --------------------------------------------------------------------------
   The scope note - a brand-tinted band carrying a figure the operator needs before
   they act (the closure cost awaiting QC approval, say).

   The screens used to open with an explanatory one of these as well; those were removed
   because they said the same thing on every visit and pushed the work below the fold.
   Keep this class for a note that carries *data*, not for one that explains the screen.

   The prototype's `.hero-note`: brand-tinted, not white. `alert alert-light border` put a
   white panel on a near-white page, so it was the least visible thing on it. Blue also
   separates it from the red/amber alerts, which carry outcomes rather than context.

   `.alert` ships `--ins-alert-border: 0 solid ...`, which is why the old markup needed the
   `border` utility on top; that utility is `!important` and would repaint the border grey,
   so it must be dropped when this class is used.
   -------------------------------------------------------------------------- */
.aira-note {
    --ins-alert-bg: var(--aira-brand-50);
    --ins-alert-border-color: #cfe2f8;
    --ins-alert-border: 1px solid var(--ins-alert-border-color);
    --ins-alert-color: var(--aira-brand-600);
    line-height: 1.6;
}

/* The second half of the sentence is context, not a different subject - keeping it grey on a
   blue ground read as two unrelated messages. `.text-muted` is !important, so this must be. */
.aira-note .text-muted {
    color: color-mix(in srgb, var(--aira-brand-600) 74%, transparent) !important;
}

html[data-bs-theme="dark"] .aira-note {
    --ins-alert-bg: color-mix(in srgb, var(--ins-primary) 14%, transparent);
    --ins-alert-border-color: color-mix(in srgb, var(--ins-primary) 34%, transparent);
    --ins-alert-color: #b9d4f5;
}

html[data-bs-theme="dark"] .aira-note .text-muted {
    color: color-mix(in srgb, #b9d4f5 76%, transparent) !important;
}

/* --------------------------------------------------------------------------
   Stage chip - used anywhere a ticket stage is displayed.
   -------------------------------------------------------------------------- */
.stage-chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .15rem .55rem;
    border-radius: 999px;
    font-size: .75rem;
    font-weight: 600;
    line-height: 1.6;
    white-space: nowrap;
    color: var(--stage-color, var(--ins-secondary-color));
    background: color-mix(in srgb, var(--stage-color, #888) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--stage-color, #888) 28%, transparent);
}

.stage-chip::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
}

/* --------------------------------------------------------------------------
   Kanban board. The column is the drop target, so it must keep a minimum
   height even when empty - otherwise a board with one empty column cannot be
   dropped into at all.
   -------------------------------------------------------------------------- */
.kanban-board {
    display: flex;
    gap: .8125rem;
    overflow-x: auto;
    padding-bottom: .5rem;
    align-items: stretch;
}

/* The column is a tinted panel and the cards on it are white. That contrast is the whole
   reason a board reads as a board: on --ins-tertiary-bg (#f6f7fb) against a #f5f6f8 page,
   with cards painted --ins-body-bg, all three planes were the same grey and the columns
   dissolved into the page. */
.kanban-column {
    flex: 0 0 300px;
    max-width: 300px;
    background: var(--aira-board-bg);
    border: 1px solid var(--aira-line);
    border-radius: var(--ins-border-radius-lg);
    display: flex;
    flex-direction: column;
}

.kanban-column-head {
    display: flex;
    align-items: center;
    gap: .5625rem;
    padding: .6875rem .8125rem;
    border-bottom: 1px solid var(--aira-line);
    border-radius: var(--ins-border-radius-lg) var(--ins-border-radius-lg) 0 0;
    font-weight: 700;
    font-size: .75rem;
    letter-spacing: .01em;
    line-height: 1.3;
    color: var(--aira-ink);
}

/* Cards are laid out by the column, not by their own margin, so the last one leaves no dead
   space above `.kanban-more`. The body is the drop target and grows to fill its stretched
   column, never shrinking below its own cards. */
.kanban-column-body {
    padding: .625rem;
    flex: 1 0 auto;
    min-height: 120px;
    display: flex;
    flex-direction: column;
    gap: .5625rem;
}

/* Drop feedback is set by the board script from the server's allowed-transition
   map, so an illegal drop is refused visually before it is refused by the API. */
.kanban-column.is-drop-allowed { outline: 2px dashed var(--aira-brand-400); outline-offset: -2px; }
.kanban-column.is-drop-denied  { outline: 2px dashed var(--ins-danger); outline-offset: -2px; }

.kanban-card {
    background: var(--ins-secondary-bg);
    border: 1px solid var(--aira-line);
    border-radius: 9px;
    padding: .6875rem .75rem;
    box-shadow: var(--aira-shadow-1);
    cursor: grab;
    transition: box-shadow .14s, transform .14s;
    /* A card must not stretch to the column's width-minus-padding through some other
       flex rule, nor shrink under its own content when the column is full. */
    flex: 0 0 auto;
}

.kanban-card:hover { box-shadow: var(--aira-shadow-2); transform: translateY(-1px); }

/* A ticket that is read-only for this user still shows on the board, but must
   not look draggable. */
.kanban-card.is-locked { cursor: default; opacity: .78; }

/* --------------------------------------------------------------------------
   Ageing indicator - shared by the board card and the ticket list.
   -------------------------------------------------------------------------- */
.age-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; }
.age-dot.age-ok    { background: var(--ins-success); }
.age-dot.age-warn  { background: var(--ins-warning); }
.age-dot.age-crit  { background: var(--ins-danger); }

/* --------------------------------------------------------------------------
   Configuration - the master rail and the SLA matrix.
   -------------------------------------------------------------------------- */
#master-rail {
    max-height: calc(100vh - 320px);
    min-height: 18rem;
    overflow-y: auto;
    /* Bootstrap's .nav is flex-wrap: wrap. With flex-direction: column and a capped height
       that wraps the list into a second column the moment the viewport is short, which
       clips half the masters. It has to scroll instead. */
    flex-wrap: nowrap;
}

#master-rail .nav-link {
    font-size: .8125rem;
    border-radius: var(--ins-border-radius);
}

#master-rail .nav-link.active .badge {
    background: rgba(255, 255, 255, .2) !important;
    color: #fff !important;
}

/* The customer register's "25 rows" is DataTables' own markup drawn outside its table
   (configuration.js, buildPageSize). Every other master has its table's row straight under the
   control's; here the table is a second .dt-container, pulled up .75rem by the theme and opened
   by an empty layout row, which leaves the gap .25rem short. Padding, because a margin here
   would collapse into the rows' own. */
[data-master-page-size] { padding-bottom: .25rem; }

/* The SLA matrix grows a column per priority, so the complaint-type column has
   to stay put while the rest scrolls - otherwise a scrolled cell has no label. */
.sla-matrix {
    max-height: calc(100vh - 420px);
}

/* Two frozen columns, not one. The serial leads the row, so it takes left:0 and the complaint
   type is pushed out by exactly its width - left:0 on both would stack them and the type name
   would scroll out from under its own serial. */
.sla-matrix-sr {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--ins-body-bg);
    width: 4rem;
    min-width: 4rem;
}

.sla-matrix-head {
    position: sticky;
    left: 4rem;
    z-index: 2;
    background: var(--ins-body-bg);
    min-width: 12rem;
}

.sla-matrix thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--ins-tertiary-bg);
}

.sla-matrix thead th.sla-matrix-head,
.sla-matrix thead th.sla-matrix-sr {
    z-index: 4;
    background: var(--ins-tertiary-bg);
}

.sla-cell {
    display: inline-flex;
    align-items: baseline;
    gap: .25rem;
    font-size: .8125rem;
    font-weight: 600;
    white-space: nowrap;
}

/* Response and resolution are two different promises. Colouring them apart is
   what makes a whole row readable at a glance instead of one number at a time. */
.sla-cell-response { color: var(--aira-brand-400); }
.sla-cell-resolution { color: var(--ins-success); }
.sla-cell-sep { color: var(--ins-secondary-color); font-weight: 400; }
.sla-unit { font-size: .6875rem; font-weight: 400; opacity: .8; }

/* An uncovered pair is a gap in the contract, not an empty cell. It is shaded so
   the holes in the grid are visible without reading every value. */
.sla-cell-empty {
    background: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 5px,
        color-mix(in srgb, var(--ins-secondary-color) 10%, transparent) 5px,
        color-mix(in srgb, var(--ins-secondary-color) 10%, transparent) 10px
    );
}

.sla-line-off td:not(:nth-child(-n+2)) { opacity: .45; }

/* --------------------------------------------------------------------------
   Coming soon / in-shell 404. The mark is the prototype's gradient tile at the
   size the card wants, so a page that does not exist yet still looks built.
   -------------------------------------------------------------------------- */
.aira-coming-soon-mark {
    flex: 0 0 52px;
    width: 52px;
    height: 52px;
    border-radius: 13px;
    display: grid;
    place-items: center;
    color: #fff;
    background: linear-gradient(150deg, var(--aira-brand-400), var(--aira-brand));
}

.aira-coming-soon-mark svg {
    width: 24px;
    height: 24px;
}

/* A 404 is not an announcement. Same tile, drained of the brand. */
.aira-coming-soon-mark-muted {
    background: var(--ins-tertiary-bg);
    color: var(--ins-secondary-color);
    border: 1px solid var(--ins-border-color);
}

/* --------------------------------------------------------------------------
   Toasts.

   Top-right, clear of the topbar, above the modal a save was very likely
   launched from. One corner for every outcome in the product: an operator
   learns a single place to look instead of tracking two.

   The dialog language (SweetAlert2) is still used for confirmations, which are
   decisions and should block. A toast is a receipt and must not.

   The tone is carried three ways at once - a filled icon, a tinted card and the
   message set bold in the tone's own emphasis colour - because a toast is read
   out of the corner of the eye while the operator is still looking at the
   board. The earlier white card with a hairline border was missed entirely.
   -------------------------------------------------------------------------- */
.aira-toaster {
    position: fixed;
    right: 1rem;
    /* Below the topbar rather than over it - the search box and the profile menu
       are exactly what an operator reaches for after reading a refusal. */
    top: calc(var(--ins-topbar-height, 70px) + .75rem);
    /* A save refused from inside a dialog has to be readable without closing the dialog -
       including the third dialog in a stack. See --aira-z-panel for the arithmetic. */
    z-index: var(--aira-z-toast);
    display: flex;
    flex-direction: column;
    gap: .5rem;
    width: min(26rem, calc(100vw - 2rem));
    pointer-events: none;
}

.aira-toast {
    /* Overridden per tone below; the fallbacks keep an unknown tone legible. */
    --aira-toast-tone: var(--ins-secondary-color);
    --aira-toast-tone-rgb: 108, 117, 125;

    /* Coloured ink, mixed rather than picked. The theme's own -text-emphasis pairs sit a shade
       off the tone itself - warning is #d29c40 - which over a tinted card is 2.3:1 and cannot be
       read. Mixing the tone INTO the body ink keeps both: dark tinted ink in light mode, light
       tinted ink in dark, from one declaration. The plain colour above it is the fallback for a
       browser without color-mix. */
    --aira-toast-text: var(--ins-body-color);

    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: .625rem;
    padding: .75rem .75rem .75rem .875rem;
    border-radius: .5rem;
    /* A tint layered over an opaque card, not a translucent card: it has to read the
       same over the board, over a modal backdrop and in dark mode. */
    background-color: var(--ins-secondary-bg);
    background-image: linear-gradient(90deg,
        rgba(var(--aira-toast-tone-rgb), .20), rgba(var(--aira-toast-tone-rgb), .10));
    color: var(--aira-toast-text);
    color: color-mix(in srgb, var(--aira-toast-tone) 35%, var(--aira-toast-text));
    border: 1px solid rgba(var(--aira-toast-tone-rgb), .45);
    border-left: .25rem solid var(--aira-toast-tone);
    box-shadow: 0 .5rem 1.5rem rgba(15, 23, 42, .18), 0 .125rem .25rem rgba(15, 23, 42, .08);
    font-size: .875rem;
    line-height: 1.45;
    animation: aira-toast-in .22s cubic-bezier(.21, 1.02, .73, 1) both;
}

.aira-toast.is-leaving {
    animation: aira-toast-out .2s cubic-bezier(.06, .71, .55, 1) both;
}

/* In from the edge it sits against, so the eye is pulled to the right-hand corner. */
@keyframes aira-toast-in {
    from { opacity: 0; transform: translate3d(1rem, -.25rem, 0) scale(.96); }
    to   { opacity: 1; transform: none; }
}

@keyframes aira-toast-out {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translate3d(1rem, 0, 0) scale(.96); }
}

/* The icon carries the verdict, so the message never has to open with "Error:". */
.aira-toast-icon {
    flex: 0 0 1.375rem;
    width: 1.375rem;
    height: 1.375rem;
    margin-top: .0625rem;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--aira-toast-tone);
    color: #fff;
}

.aira-toast-icon svg {
    width: .8125rem;
    height: .8125rem;
}

.aira-toast-message {
    flex: 1 1 auto;
    min-width: 0;
    padding-top: .0625rem;
    font-weight: 600;
    /* A refusal can name a customer whose name has no spaces to break on. */
    overflow-wrap: anywhere;
}

.aira-toast-close {
    flex: 0 0 auto;
    padding: 0;
    width: 1.125rem;
    height: 1.125rem;
    border: 0;
    background: none;
    border-radius: .25rem;
    color: inherit;
    opacity: .55;
    transition: opacity .15s ease-in-out;
}

.aira-toast:hover .aira-toast-close,
.aira-toast-close:focus-visible { opacity: 1; }

.aira-toast-close svg { width: .875rem; height: .875rem; }

/* A tone is two values: the colour itself for the icon and the accent, and its rgb triple for
   every translucent layer. The ink is mixed from the first, above. */
.aira-toast-success {
    --aira-toast-tone: var(--ins-success);
    --aira-toast-tone-rgb: var(--ins-success-rgb);
}

.aira-toast-error {
    --aira-toast-tone: var(--ins-danger);
    --aira-toast-tone-rgb: var(--ins-danger-rgb);
}

.aira-toast-warning {
    --aira-toast-tone: var(--ins-warning);
    --aira-toast-tone-rgb: var(--ins-warning-rgb);
}

.aira-toast-info {
    --aira-toast-tone: var(--ins-info);
    --aira-toast-tone-rgb: var(--ins-info-rgb);
}

@media (prefers-reduced-motion: reduce) {
    .aira-toast, .aira-toast.is-leaving { animation-duration: .01ms; }
}

/* --------------------------------------------------------------------------
   The Sr No column, on every list table.

   Narrow and fixed so four digits cannot shove the first real column off a phone, and
   CENTRED under its own heading - the column is two characters of body text wide, so a
   right-aligned figure sat hard against the customer name beside it and read as part of that
   cell rather than as the row's number. Tabular figures keep the units in line down the page
   anyway - a 9 sitting under the 1 of a 10 is how a column of numbers stops being scannable -
   which is what the right edge was bought for, and centring does not cost it. Muted because
   the serial is a position, not data, and must not compete with the row it leads.

   The number itself is never written in a view: Utils/SerialNo.cs for a server-rendered
   table, Aira.tables.serial for a DataTables-rendered one.

   A total or sub-total row carries an EMPTY `.sr-col` cell rather than a number: it is not a
   record, so it has no position, and numbering it makes the count one too many.
   -------------------------------------------------------------------------- */
.sr-col {
    width: 4rem;
    text-align: center;
    white-space: nowrap;
    color: var(--aira-muted);
    font-variant-numeric: tabular-nums;
}

/* The rows region while a filter is in flight. Dimmed rather than emptied (the shared rule under
   *Loading*): a table that blanks between keystrokes reads as "no matches" for the fraction of a
   second before the real answer arrives, and the operator stops typing. */
[data-master-rows].is-loading { pointer-events: none; }

/* --------------------------------------------------------------------------
   Forms - the two states the compiled scale has no rule for.

   Size, radius, focus ring and label ink are all compiled, from the control metrics in
   wwwroot/scss/_aira-brand.scss. Do NOT restate padding, font-size or border-radius for a
   form control here: two sources for one measurement is how the create screen and the
   Settings screen start disagreeing about how tall a field is.
   -------------------------------------------------------------------------- */

/* Read-only is not disabled. The value is real and the operator is meant to read it - the
   ticket number, the derived chargeable flag - it simply is not
   theirs to type. Bootstrap styles :disabled and stops there, so a readonly field was
   indistinguishable from an editable one until somebody clicked into it and nothing
   happened. The prototype drops it to the second surface and takes the focus ring off. */
.form-control[readonly]:not(.form-control-plaintext) {
    background-color: var(--aira-surface-2);
    color: var(--aira-ink-2);
    cursor: default;
}

/* Not a refused one: the engine focuses the field the server refused (C13's fixed marketing person
   is read-only), and this rule's weight took the red off it at exactly that moment. */
.form-control[readonly]:not(.form-control-plaintext):not(.is-invalid):hover,
.form-control[readonly]:not(.form-control-plaintext):not(.is-invalid):focus {
    border-color: var(--ins-border-color);
    box-shadow: none;
}

/* --------------------------------------------------------------------------
   Validation.

   The theme already ships Bootstrap's .is-invalid / .invalid-feedback pair, so
   there is nothing to restyle - only the two places the theme's own layout
   leaves the message in the wrong spot. See app/js/aira-validation.js.
   -------------------------------------------------------------------------- */

/* The engine inserts the feedback *immediately* after its control so the source order
   matches the reading order, which puts it above the field's help text. Without this the
   two run together and the red sentence stops looking like the answer to what went wrong. */
.invalid-feedback { margin-bottom: .125rem; }

.invalid-feedback:empty { display: none !important; }

/* A switch turns its own label red rather than pushing a sentence between it and the next
   control - the message there is always about the label itself. */
.form-check-input.is-invalid ~ .form-check-label { color: var(--ins-form-invalid-color); }

/* There is no validation glyph to offset: $enable-validation-icons is off in _variables.scss,
   because its padding ate the room the value needed - see "A refused field keeps its room" in
   .claude/docs/forms.md. Do not add one back here for a single screen. */

/* ==========================================================================
   Phase 3 - the ticket flow.

   Everything below belongs to the board, the complaint page and the queues.
   Colours come from the --stage-* tokens at the top of this file, so a stage
   can never appear in two shades on two screens.
   ========================================================================== */

/* --------------------------------------------------------------------------
   The period rail.

   A row of pills over the two dates rather than a <select> beside them. The
   select carried "Custom range" as a pickable option and picking it did
   nothing at all - no dates, no reload, no focus - so the one gesture that
   says "I am about to type my own dates" reached dead code, and the control
   then sat there claiming Custom while the query was still last month.

   The lit pill is always DERIVED from the two dates, so there is still exactly
   one statement of what is being asked for. Custom is a state the operator
   arms, never a value that is read or posted.

   No sizes are restated: a pill IS a .btn.btn-sm, so its padding and font come
   from the compiled control metrics in _aira-brand.scss (hard rule 10a).
   -------------------------------------------------------------------------- */
.aira-period-rail {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
}

.aira-period-pill { border-radius: 999px; }

.aira-period-pill.is-active,
.aira-period-pill.is-active:hover,
.aira-period-pill.is-active:focus {
    background: var(--ins-primary);
    border-color: var(--ins-primary);
    color: #fff;
}

.aira-period-read { color: var(--aira-muted); }

/* The two dates under the rail, side by side - the panel is one column of sections, and a date
   is narrow. Their labels are the section's sub-labels, so they keep the compiled .form-label
   size (rule 10a) and only step back in colour. */
.aira-period-dates {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .5rem;
    margin-top: .5rem;
}

.aira-period-dates .form-label {
    margin-bottom: .25rem;
    color: var(--aira-muted);
}

/* Armed, and waiting for the operator to type. Deliberately the same ring the
   multi-select trigger wears when its panel is open, so the two read as one
   product rather than as two controls that happen to share a bar. */
.aira-filterbar[data-period-custom="on"] [data-period-dates] {
    box-shadow: 0 0 0 .15rem color-mix(in srgb, var(--ins-primary) 18%, transparent);
    border-radius: var(--ins-border-radius);
}

/* --------------------------------------------------------------------------
   The multi-select (app/js/aira-multiselect.js) - its first rules. The rows, ticks, footer and
   groups are the block further down (.aira-ms-opt, -box, -act, -group); the rules here are the
   trigger, panel and list properties that block does not restate, and both apply.

   Why it is ours and not the theme's Choices skin is in app/js/aira-multiselect.js, and it
   comes down to one line of that plugin: its container template copies only its own class, so
   `form-select-sm` never reaches the box it renders, and the theme's _choice.scss has no `-sm`
   variant to fall back to. The widget is architecturally incapable of being the toolbar size,
   and forcing it would mean either editing a theme file or restating a compiled control
   metric here - which is exactly what rule 10a exists to stop.

   So nothing below states a height, a padding or a font size for the control. The trigger IS
   a .form-select.form-select-sm - a <button> wearing the select's own classes - which is what
   lines it up with the Age select and the two date inputs beside it without one measurement
   being typed twice. The panel is a .dropdown-menu, so its ground, border, radius and shadow
   arrive from --ins-dropdown-* in both colour modes for free.

   The native <select multiple> underneath is the source of truth: it keeps the name the server
   reads, it is what the label points at, and everything here is a view of it.
   -------------------------------------------------------------------------- */

/* One line of text and the select's own caret, clipped rather than wrapped. A box that grows
   with its contents would move every control on the bar the moment somebody ticks a fourth
   value - and the summary is never shortened in JS, so what a screen reader announces stays
   whole even where the box has cut it off. */
.aira-ms-trigger {
    display: flex;
    align-items: center;
    overflow: hidden;
}

.aira-ms-summary {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.aira-ms-trigger[aria-expanded="true"] {
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 .15rem color-mix(in srgb, var(--ins-primary) 18%, transparent);
}

/* Only the ceiling is ours. The component copies the trigger's measured width onto the
   panel's min-width on every open, because Popper's fixed strategy - which is what keeps the
   panel out of the drill dialog's scroll clip, where no z-index would have saved it - takes
   the panel out of any layout that could have sized it.

   Do NOT give this rule a `display`. .dropdown-menu is display:none until Bootstrap adds
   .show, both at one class of specificity, and this file loads last: a display here would win
   the hidden case and leave all nine panels on the bar permanently open. */
.aira-ms-panel { max-width: 22rem; }

/* Search box, rows and note share one left edge, on the theme's own dropdown item inset. */
.aira-ms-search { padding: .125rem var(--ins-dropdown-item-padding-x) .5rem; }

.aira-ms-list {
    max-height: 15rem;
    overflow-y: auto;
    outline: none;
    /* The bar renders inside the drill dialog's scrolling body as well as on the page.
       Without this, hitting the end of the list carries on scrolling the dialog underneath
       and the panel walks off the screen mid-pick. */
    overscroll-behavior: contain;
}

.aira-ms-text {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* --------------------------------------------------------------------------
   Kanban board
   -------------------------------------------------------------------------- */
.kanban-board {
    /* The page scrolls, not the columns, and every lane runs the board's full height
       (`align-items: stretch` above), so a short column still takes a drop level with the
       bottom of a long one. */
    padding-bottom: .75rem;
    /* No scroll-snap. With columns 313px apart, `proximity` refused to rest anywhere but a column
       edge: a 100px wheel notch or a scrollbar drag let go mid-column was pulled back to where it
       started or flung a whole column on, so sideways scrolling juddered instead of gliding.
       A swipe past the last column scrolls nothing further rather than navigating the tab back. */
    overscroll-behavior-x: contain;
}

/* The stage's colour, as the prototype draws it: a small rounded square, not a dot. It is the
   only place the stage colour appears on the board, so it carries full saturation. */
.kanban-column-dot {
    width: 9px;
    height: 9px;
    border-radius: 2px;
    background: var(--stage-color, var(--ins-secondary-color));
    flex: 0 0 9px;
}

.kanban-column-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A white pill on the tinted head - the count has to stay legible when the column is the one
   with 34 cards in it. */
.kanban-column-count {
    margin-left: auto;
    flex: 0 0 auto;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--aira-line);
    color: var(--aira-ink-2);
    font-size: .6875rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.45;
}

.kanban-column-hidden {
    background: transparent;
    border-style: dashed;
    border-color: var(--aira-line-2);
}

.kanban-column-hidden .kanban-column-head {
    border-bottom-color: transparent;
    color: var(--aira-muted);
    text-transform: uppercase;
    letter-spacing: .09em;
}

/* Only while something is actually being dragged. Painting every column at rest would make
   the board look like a permanent error state. */
.kanban-board.is-dragging .kanban-column.is-drop-denied { opacity: .55; }

.kanban-board.is-dragging .kanban-column.is-drop-allowed {
    background: color-mix(in srgb, var(--ins-primary) 6%, var(--ins-tertiary-bg));
}

.kanban-card:focus-visible {
    outline: 2px solid var(--ins-primary);
    outline-offset: 1px;
}

/* The gap a dragged card will drop into. It follows the pointer and the card is dropped exactly
   there, so it reads as a slot rather than a faded copy of the card: a faint card on the column
   plane could be mistaken for a real one. Its height is set from the dragged card's own.

   No entrance animation: the gap is moved by re-inserting it, which restarts any animation on every
   move, and a gap that fades in each time it changes place reads as flicker - the opposite of the
   instant response it exists for. */
.kanban-drop-slot {
    flex: 0 0 auto;
    border-radius: 9px;
    border: 2px dashed color-mix(in srgb, var(--ins-primary) 55%, transparent);
    background: color-mix(in srgb, var(--ins-primary) 7%, transparent);
}

/* The card being dragged is hidden where it sat - the gap has taken its place - and the browser's
   drag image, a copy of it, is what follows the pointer. */
.kanban-card.is-drag-source { display: none; }

/* The drag image. Rendered off-screen for one frame and photographed by the browser, so it needs its
   lift in its own rules rather than borrowed from :hover. */
.kanban-card.kanban-drag-preview {
    box-shadow: 0 14px 30px rgba(15, 23, 42, .22);
    border-color: color-mix(in srgb, var(--ins-primary) 45%, var(--aira-line));
    transform: none;
}

/* A hover lift under the pointer mid-drag nudges the cards the gap is measured against. */
.kanban-board.is-dragging .kanban-card:hover { transform: none; box-shadow: var(--aira-shadow-1); }

/* The column the gap is in right now, stronger than the other columns that would also take it. */
.kanban-board.is-dragging .kanban-column.is-drop-over {
    background: color-mix(in srgb, var(--ins-primary) 12%, var(--ins-tertiary-bg));
}

/* An empty column's "No complaints here" would otherwise sit beside the gap being offered. */
.kanban-column-body:has(> .kanban-drop-slot) > .kanban-empty { display: none; }

/* Dropped and on its way to the server. Not draggable again until the answer is in: a second drag
   of the same card would post a placement against a position that is about to change. */
.kanban-card.is-saving {
    pointer-events: none;
    opacity: .72;
}

.kanban-card-no {
    font-size: .6875rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--ins-primary);
    letter-spacing: .01em;
    line-height: 1.4;
}

/* The customer is what an operator scans a column for, so it is the only thing on the card
   painted in full body ink. */
.kanban-card-customer {
    font-size: .8125rem;
    font-weight: 650;
    margin: .1875rem 0 .125rem;
    line-height: 1.35;
    color: var(--aira-ink);
}

.kanban-card-desc {
    font-size: .71875rem;
    line-height: 1.45;
    /* --aira-ink-2, not --ins-secondary-color: the description is meant to be read, and the
       theme's muted grey is for text that is not. */
    color: var(--aira-ink-2);
    margin: 0;
    /* Two lines is the point where a card stops being scannable. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.kanban-card-flag {
    display: flex;
    align-items: center;
    gap: .375rem;
    margin-top: .5625rem;
    padding: .3125rem .5625rem;
    border-radius: 6px;
    font-size: .65625rem;
    font-weight: 650;
    line-height: 1.35;
}

.kanban-card-flag::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    opacity: .7;
    flex: 0 0 6px;
}

.kanban-card-flag.is-good {
    background: var(--ins-success-bg-subtle);
    color: var(--ins-success-text-emphasis);
    border: 1px solid var(--ins-success-border-subtle);
}

.kanban-card-flag.is-warn {
    background: var(--ins-warning-bg-subtle);
    color: var(--ins-warning-text-emphasis);
    border: 1px solid var(--ins-warning-border-subtle);
}

.kanban-card-flag.is-bad {
    background: var(--ins-danger-bg-subtle);
    color: var(--ins-danger-text-emphasis);
    border: 1px solid var(--ins-danger-border-subtle);
}

/* One rule, ruled off from the body above it. The attachment / follow-up / SLA markers used
   to sit on their own line below the footer, which made a card with any of them a third
   taller than one without - so a column of them scanned as ragged rather than as a list. */
.kanban-card-foot {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: .5rem;
    margin-top: .5625rem;
    padding-top: .5625rem;
    border-top: 1px solid var(--aira-line);
    overflow: hidden;
}

.kanban-card-age {
    margin-left: auto;
    flex: 0 0 auto;
    font-size: .65625rem;
    font-weight: 600;
    color: var(--aira-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

.kanban-meta {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    flex: 0 0 auto;
    font-size: .65625rem;
    color: var(--aira-muted);
    white-space: nowrap;
}

.kanban-meta svg { width: 12px; height: 12px; }
.kanban-meta.is-breached { color: var(--ins-danger); font-weight: 700; }

.kanban-empty,
.kanban-more {
    text-align: center;
    font-size: .71875rem;
    color: var(--aira-muted);
    padding: 1.125rem .375rem;
}

.kanban-more {
    padding: .5625rem;
    border: 1px dashed var(--aira-line-2);
    border-radius: 9px;
    background: var(--ins-secondary-bg);
    color: var(--aira-ink-2);
    font-weight: 600;
}

/* A column that is still loading has to hold its height, or the board reflows under the
   cursor the moment the first response lands. The shimmer is the shared one (*Loading* below);
   on the board's grey ground it is card-coloured, which is what a card-shaped block should be. */
.kanban-skeleton {
    --aira-skel-base: var(--ins-secondary-bg);
    --aira-skel-glint: var(--ins-tertiary-bg);
    height: 76px;
    flex: 0 0 auto;
    border-radius: 9px;
}

/* --------------------------------------------------------------------------
   Loading - Aira.loading in app/js/aira.js, ubold-ui.md *Loading*.

   ONE shimmer for the product. It was two copies of one gradient (the board's
   and the Tickets list's) and a third area - the dashboard - that had none and
   showed a title over nothing. A host holds its height through --aira-skel-h, so
   nothing jumps when the figures land. Colours come only from --ins-secondary-bg,
   --ins-tertiary-bg and --aira-line-2, so dark mode follows by itself: on a white
   card the theme's tertiary ground is a near-invisible #f6f7fb, so the light
   base is mixed a step towards the line colour.
   -------------------------------------------------------------------------- */
html:not([data-bs-theme="dark"]) {
    --aira-skel-base: color-mix(in srgb, var(--aira-line-2) 50%, var(--ins-tertiary-bg));
    --aira-skel-glint: var(--ins-tertiary-bg);
}

html[data-bs-theme="dark"] {
    --aira-skel-base: var(--ins-tertiary-bg);
    --aira-skel-glint: var(--aira-line-2);
}

.aira-skel,
.kanban-skeleton {
    background: linear-gradient(90deg,
        var(--aira-skel-base) 25%,
        var(--aira-skel-glint) 37%,
        var(--aira-skel-base) 63%);
    background-size: 400% 100%;
    border-radius: 4px;
    animation: aira-shimmer 1.3s ease-in-out infinite;
}

@keyframes aira-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

/* A shimmer is motion, and says nothing a still block does not. */
@media (prefers-reduced-motion: reduce) {
    .aira-skel,
    .kanban-skeleton { animation: none; }
}

/* The containers are not blocks of their own - their parts are. */
.aira-skel.aira-skel--chart,
.aira-skel.aira-skel--rows {
    background: none;
    animation: none;
    border-radius: 0;
}

/* Five bars seated on a baseline, at the height the chart will be. */
.aira-skel--chart {
    display: flex;
    align-items: flex-end;
    gap: 6%;
    height: var(--aira-skel-h, 240px);
    padding: 1rem 6% 0;
    border-bottom: 1px solid var(--aira-line-2);
}

.aira-skel--chart > i {
    flex: 1 1 0;
    border-radius: 4px 4px 0 0;
}

/* The ring a donut or a gauge will draw, at its own diameter. */
.aira-skel--donut {
    width: var(--donut-size, var(--aira-skel-h, 180px));
    max-width: 100%;
    aspect-ratio: 1;
    margin: 0 auto;
    border-radius: 50%;
    -webkit-mask: radial-gradient(farthest-side, transparent 62%, #000 63%);
    mask: radial-gradient(farthest-side, transparent 62%, #000 63%);
}

/* A tile of the grid it waits in - .kpi-card's height, .report-card's radius. */
.aira-skel--tile {
    height: var(--aira-skel-h, 104px);
    border-radius: var(--ins-border-radius-lg, .5rem);
}

/* The dashboard's SLA card, always the last tile, carries a third figure (No target) that wraps
   under the other two at a tile's usual width - so its skeleton waits a line taller, and its row
   does not grow when it lands. */
[data-dash-tiles] > .aira-skel--tile:last-child { height: calc(var(--aira-skel-h, 104px) + 1.8rem); }

.aira-skel--rows {
    display: flex;
    flex-direction: column;
    gap: .75rem;
    padding: .75rem 1rem;
    min-height: var(--aira-skel-h, 0);
}

.aira-skel--rows > i { display: block; height: 1rem; }

/* A table body's rows: a bar per row, on a row with no rule under it. */
.aira-skel-row > td { border-bottom-color: transparent; }
.aira-skel--bar { display: block; height: 1.125rem; }

/* A line of text still to come - a count beside a heading. */
.aira-skel--line {
    display: inline-block;
    width: 9rem;
    height: .875em;
    vertical-align: middle;
}

/* A dialog body waiting for a whole form: the theme's own spinner, centred. */
.aira-skel--panel {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--aira-skel-h, 0);
    padding: 3rem 0;
}

/* A refresh dims what is there rather than emptying it: a card that empties and refills reads
   as data disappearing. aria-busy is the state; .is-loading only says this host dims. */
.is-loading[aria-busy="true"] {
    opacity: .55;
    transition: opacity .12s ease-out;
}

/* What a failed host held answers a question no longer on screen, so the failure replaces it. */
[data-aira-failed] > :not([data-aira-fail]) { display: none !important; }

/* The compact failed box, where figures were: it sits in a card rather than above a view, and
   across every column of a tile grid rather than in the first cell of it. */
.aira-ws-state.aira-load-failed {
    grid-column: 1 / -1;
    margin-bottom: 0;
}

/* A table whose draw failed counts nothing: its info and pager described the rows that went. */
[data-table-failed] .dt-info,
[data-table-failed] .dt-paging { visibility: hidden; }

/* --------------------------------------------------------------------------
   Chips shared by the board, the tables and the ticket page.
   -------------------------------------------------------------------------- */
/* Priority is a coloured dot and a coloured word, not a pill - the prototype's `.pri`.
   Four filled pills on one card footer, next to a fifth pill for the product, turned a card
   into a row of badges with the customer's name lost above them. The dot carries the
   saturation; the text carries a darker shade of the same hue so it stays readable. */
.pri-chip {
    display: inline-flex;
    align-items: center;
    gap: .3125rem;
    /* Never shrinks. It is one short word, and letting it compress inside the card footer
       squeezed it under the product chip beside it. The product chip absorbs the slack. */
    flex: 0 0 auto;
    font-size: .71875rem;
    font-weight: 600;
    white-space: nowrap;
}

.pri-chip::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--pri-dot, currentColor);
    flex: 0 0 7px;
}

.pri-low    { color: #6c7684; --pri-dot: #9aa4b2; }
.pri-normal { color: #20609f; --pri-dot: #2a78d6; }
.pri-high   { color: #b25a32; --pri-dot: #ec835a; }
.pri-urgent { color: #a52d2d; --pri-dot: #d03b3b; }

/* The dots hold; only the words are lifted off the dark ground. */
html[data-bs-theme="dark"] .pri-low    { color: #9aa4b2; }
html[data-bs-theme="dark"] .pri-normal { color: #74aae8; }
html[data-bs-theme="dark"] .pri-high   { color: #f0a583; }
html[data-bs-theme="dark"] .pri-urgent { color: #ea8484; }

.chip-muted,
.chip-info {
    display: inline-block;
    white-space: nowrap;
    vertical-align: bottom;
    padding: .1875rem .5625rem;
    border-radius: 999px;
    font-size: .71875rem;
    font-weight: 600;
    line-height: 1.4;
    border: 1px solid var(--aira-line);
    background: var(--aira-surface-2);
    color: var(--aira-ink-2);
}

.chip-info {
    background: var(--aira-brand-50);
    border-color: #cfe2f8;
    color: var(--aira-brand-600);
}

/* Only on a 300px card, where the footer also carries the priority, the markers and the age -
   this is the one item allowed to give up width, and it ellipsises when it does. In a table
   cell the same chip has room for the whole product name and must not hide half of it. */
.kanban-card-foot .chip-muted {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 8.5rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

html[data-bs-theme="dark"] .chip-info {
    background: color-mix(in srgb, var(--ins-primary) 18%, transparent);
    border-color: color-mix(in srgb, var(--ins-primary) 38%, transparent);
    color: #9cc4f2;
}

/* --------------------------------------------------------------------------
   The complaint page.
   -------------------------------------------------------------------------- */
.ticket-panel { padding: 1rem 1.25rem 0; }
.ticket-panel-head { padding-bottom: .75rem; }

.ticket-panel-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    padding: .75rem 0;
    margin-top: .5rem;
    border-top: 1px solid var(--ins-border-color);
    position: sticky;
    bottom: 0;
    background: var(--ins-body-bg);
    z-index: 2;
}

/* The progress rail. Horizontal on a wide screen, and it scrolls rather than wrapping -
   a wrapped rail reads as two workflows. */
.stage-rail {
    display: flex;
    align-items: flex-start;
    gap: 0;
    overflow-x: auto;
    padding: .5rem 0 .75rem;
    margin-bottom: .5rem;
    border-bottom: 1px solid var(--ins-border-color);
}

.stage-rail-step {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .3rem;
    min-width: 5.5rem;
    flex: 1 0 auto;
    position: relative;
    text-align: center;
}

/* The connector is drawn on the step rather than as its own element, so a step cannot end up
   with a line pointing at nothing. */
.stage-rail-step:not(:first-child)::before {
    content: "";
    position: absolute;
    top: 11px;
    right: 50%;
    left: -50%;
    height: 2px;
    background: var(--ins-border-color);
}

.stage-rail-step.is-done::before,
.stage-rail-step.is-current::before { background: var(--stage-color, var(--ins-primary)); }

.stage-rail-dot {
    position: relative;
    z-index: 1;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .625rem;
    font-weight: 700;
    background: var(--ins-secondary-bg);
    color: var(--ins-secondary-color);
    border: 2px solid var(--ins-border-color);
}

.stage-rail-dot svg { width: 12px; height: 12px; }

.stage-rail-step.is-done .stage-rail-dot {
    background: var(--stage-color);
    border-color: var(--stage-color);
    color: #fff;
}

.stage-rail-step.is-current .stage-rail-dot {
    background: var(--ins-body-bg);
    border-color: var(--stage-color);
    color: var(--stage-color);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--stage-color) 20%, transparent);
}

/* Label over timestamp. The two stack so a nine-step rail stays one row of dots rather than
   growing a second rail's worth of width per step. */
.stage-rail-body {
    display: flex;
    flex-direction: column;
    gap: .0625rem;
    min-width: 0;
}

.stage-rail-label {
    font-size: .6875rem;
    line-height: 1.25;
    color: var(--ins-secondary-color);
    max-width: 7.5rem;
}

.stage-rail-step.is-current .stage-rail-label { color: var(--ins-emphasis-color); font-weight: 600; }

/* The moment the complaint entered the stage. Never wraps: a date broken across two lines reads
   as two dates, and this rail already scrolls sideways by design. */
.stage-rail-when {
    font-size: .625rem;
    line-height: 1.2;
    color: var(--aira-muted);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* On Hold is appended to the rail, not part of it - so it is drawn as an interruption rather
   than as the next step along. */
.stage-rail-step.is-hold .stage-rail-dot {
    background: var(--ins-body-bg);
    border-color: var(--stage-hold, var(--ins-warning));
    color: var(--stage-hold, var(--ins-warning));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--stage-hold, var(--ins-warning)) 22%, transparent);
}

.stage-rail-step.is-hold .stage-rail-label {
    color: var(--stage-hold, var(--ins-warning));
    font-weight: 700;
}

/* Dashed, because the complaint did not travel this leg - it stopped. */
.stage-rail-step.is-hold::before {
    background: none;
    border-top: 2px dashed var(--stage-hold, var(--ins-warning));
    height: 0;
}

/* The mandatory-field strip. It is the difference between "why will this not go to Solved?"
   and a support call. */
.missing-strip {
    border: 1px solid var(--ins-warning-border-subtle);
    background: var(--ins-warning-bg-subtle);
    border-radius: var(--ins-border-radius);
    padding: .5rem .75rem;
    margin-bottom: .75rem;
}

.missing-strip.is-clear {
    border-color: var(--ins-success-border-subtle);
    background: var(--ins-success-bg-subtle);
}

.missing-strip-head {
    display: flex;
    align-items: center;
    gap: .375rem;
    font-size: .8125rem;
    font-weight: 600;
    color: var(--ins-warning-text-emphasis);
}

.missing-strip.is-clear .missing-strip-head { color: var(--ins-success-text-emphasis); }

.missing-strip-block {
    margin-left: auto;
    font-size: .625rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    padding: .1rem .4rem;
    border-radius: 999px;
    background: var(--ins-danger);
    color: #fff;
}

.missing-strip-row {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    align-items: baseline;
    margin-top: .375rem;
    font-size: .75rem;
}

.missing-strip-group {
    font-weight: 600;
    min-width: 10rem;
    color: var(--ins-emphasis-color);
}

.missing-strip-fields { color: var(--ins-secondary-color); }

.ticket-tabs { margin-bottom: 0; }
.ticket-tab-content { padding-top: 1rem; }

.ticket-section {
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius-lg);
    padding: .875rem 1rem;
    margin-bottom: 1rem;
}

/* The prototype's `.form-sec > h4`: brand-blue small caps over a hairline. It was 13px in
   the muted grey - the same ink as the help text under every field - with nothing beneath
   it, so a long complaint read as one undivided run of fields. The rule sits on the head
   rather than on the title because the head is a flex row carrying the pending badge and
   the Edit button: underlining the <h5> alone would underline three words mid-row.

   --ins-primary-text-emphasis, not --ins-primary: this is text, and the brand at full strength
   is #1b56a0 - a navy that all but disappears against the dark theme's ground. The emphasis
   pair is #174988 in light (the prototype's own --brand-600) and lifts to #769ac6 in dark, and
   BrandPalette.cs re-derives both when an operator picks their own colour under Settings. */
.ticket-section-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .75rem;
    padding-bottom: .5rem;
    border-bottom: 1px solid var(--ins-border-color);
}

.ticket-section-title {
    margin: 0;
    font-size: .6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--ins-primary-text-emphasis);
}

.ticket-section-blurb {
    font-size: .75rem;
    color: var(--ins-secondary-color);
    margin: -.25rem 0 .75rem;
}

.ticket-section-note {
    font-size: .75rem;
    color: var(--ins-secondary-color);
    margin: .5rem 0 0;
    padding-top: .5rem;
    border-top: 1px dashed var(--ins-border-color);
}

.ticket-section-lock {
    display: inline-flex;
    align-items: center;
    font-size: .6875rem;
    font-weight: 600;
    color: var(--ins-secondary-color);
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: 999px;
    padding: .15rem .5rem;
}

/* Both views are in the DOM; editing swaps them. A round trip to open a form the operator
   opens forty times a day is forty round trips. */
.ticket-section-form { display: none; }
.ticket-section.is-editing .ticket-section-form,
.ticket-section-form.is-open { display: block; }
.ticket-section.is-editing [data-section-read],
.ticket-section.is-editing [data-cost-read] { display: none; }
.ticket-section.is-editing { border-color: var(--ins-primary); }

/* An Edit button on a section already being edited does nothing; the bar below is the way out. */
.ticket-section.is-editing [data-section-edit],
.ticket-section.is-editing [data-cost-edit] { display: none; }

/* The editing section's own bar. Sticky to the bottom of whatever scrolls - the dialog body or
   the window - and lifted by the panel's action bar's height (measured by ticket-detail.js),
   so Save section is on screen for as long as any of the section is. Its containing block is
   the form, so once the section's end is in view it settles back into place.

   It bleeds to the section's border (the negative margins undo .ticket-section's padding) and
   paints an opaque ground, because stuck it sits over the fields scrolling beneath it. */
.ticket-section-actions {
    position: sticky;
    bottom: var(--ticket-foot-height, 0px);
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem .75rem;
    margin: 1rem -1rem -.875rem;
    padding: .625rem 1rem;
    background: var(--ins-secondary-bg);
    border-top: 1px solid var(--ins-border-color);
    border-radius: 0 0 var(--ins-border-radius-lg) var(--ins-border-radius-lg);
    box-shadow: 0 -.5rem 1rem -.75rem rgba(0, 0, 0, .2);
}

.ticket-section-status {
    display: flex;
    align-items: center;
    gap: .375rem;
    min-width: 0;
    font-size: .8125rem;
    color: var(--ins-secondary-color);
}

.ticket-section-status::before {
    content: "";
    flex: none;
    width: .5rem;
    height: .5rem;
    border-radius: 50%;
    background: var(--ins-primary);
}

.ticket-section-buttons {
    display: flex;
    gap: .5rem;
    margin-left: auto;
}

/* Off the dirty guard's own .is-dirty on the form, so the words cannot disagree with whether
   Save section is alive. */
.ticket-section-status-dirty,
.ticket-section-status-nudge { display: none; }

.ticket-section-form.is-dirty .ticket-section-status-clean { display: none; }
.ticket-section-form.is-dirty .ticket-section-status-dirty {
    display: inline;
    font-weight: 600;
    color: var(--ins-warning-text-emphasis);
}
.ticket-section-form.is-dirty .ticket-section-status::before { background: var(--ins-warning); }

/* A move, a close or a second Edit was held because of these changes. The reason is painted
   on the bar the operator has to use next, not put in a toast. */
.ticket-section-actions.is-nudged .ticket-section-status-clean,
.ticket-section-actions.is-nudged .ticket-section-status-dirty { display: none; }
.ticket-section-actions.is-nudged .ticket-section-status-nudge {
    display: inline;
    font-weight: 600;
    color: var(--ins-danger-text-emphasis);
}
.ticket-section-actions.is-nudged .ticket-section-status::before { background: var(--ins-danger); }
.ticket-section-actions.is-nudged { animation: aira-section-nudge .45s ease-in-out 2; }

@keyframes aira-section-nudge {
    50% { box-shadow: 0 -.5rem 1rem -.75rem rgba(0, 0, 0, .2), inset 0 0 0 2px rgba(var(--ins-danger-rgb), .55); }
}

@media (prefers-reduced-motion: reduce) {
    .ticket-section-actions.is-nudged { animation: none; }
}

/* The read view: a two-column definition list that collapses to one on a phone. */
.ticket-kv {
    display: grid;
    grid-template-columns: minmax(9rem, 12rem) 1fr;
    gap: .25rem 1rem;
    margin: 0;
    font-size: .8125rem;
}

.ticket-kv dt {
    font-weight: 500;
    color: var(--ins-secondary-color);
    font-size: .75rem;
    padding-top: .1rem;
}

.ticket-kv dd { margin: 0 0 .25rem; }
.ticket-kv-blank { color: var(--ins-secondary-color); font-style: italic; opacity: .8; }

@media (max-width: 575.98px) {
    .ticket-kv { grid-template-columns: 1fr; }
    .ticket-kv dd { margin-bottom: .5rem; }
}

.ticket-empty {
    text-align: center;
    font-size: .8125rem;
    color: var(--ins-secondary-color);
    background: var(--ins-tertiary-bg);
    border: 1px dashed var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    padding: 1.25rem 1rem;
}

/* --------------------------------------------------------------------------
   Timeline - the complaint's whole story in one list (_TicketLog.cshtml).

   One rail down the left, a dated pill where the day changes, a coloured
   marker per entry. Each entry sets --tl-color: the target stage's own colour
   on a move, a theme token for everything else.

   The marker and the day pill are opaque on --ins-secondary-bg, which is the
   ground of both callers (the dialog and the card on the full page) - that is
   what lets the rail pass behind them rather than through them.
   -------------------------------------------------------------------------- */
.tl-total {
    font-size: .75rem;
    color: var(--ins-secondary-color);
    white-space: nowrap;
}

/* Two buttons beside the title leave a phone no room; they drop under it instead. */
.tl-section .ticket-section-head { flex-wrap: wrap; }

.tl-head-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
    margin-left: auto;
}

.tl-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
    margin-bottom: 1rem;
}

.tl-filter,
.tl-order {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    padding: .3125rem .6875rem;
    font-size: .75rem;
    font-weight: 600;
    line-height: 1.2;
    color: var(--ins-secondary-color);
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: 999px;
    cursor: pointer;
    transition: color .15s ease, background-color .15s ease, border-color .15s ease;
}

.tl-filter svg,
.tl-order svg { width: .875rem; height: .875rem; }

.tl-filter:hover,
.tl-order:hover {
    color: var(--ins-emphasis-color);
    border-color: color-mix(in srgb, var(--ins-primary) 45%, var(--ins-border-color));
}

.tl-filter:focus-visible,
.tl-order:focus-visible,
.tl-more:focus-visible {
    outline: 2px solid var(--ins-primary);
    outline-offset: 2px;
}

.tl-filter.active {
    color: #fff;
    background: var(--ins-primary);
    border-color: var(--ins-primary);
}

.tl-filter-count {
    min-width: 1.25rem;
    padding: .0625rem .375rem;
    font-size: .6875rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
    border-radius: 999px;
    background: var(--ins-tertiary-bg);
}

.tl-filter.active .tl-filter-count { background: rgba(255, 255, 255, .22); }

.tl { position: relative; }

.tl::before {
    content: "";
    position: absolute;
    top: .75rem;
    bottom: .75rem;
    left: calc(1rem - 1px);
    width: 2px;
    border-radius: 1px;
    background: var(--ins-border-color);
}

.tl-day { position: relative; }
.tl-day + .tl-day { margin-top: .375rem; }

.tl-day-head {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    margin: 0 0 .875rem;
    padding: .25rem .75rem;
    font-size: .6875rem;
    font-weight: 700;
    line-height: 1.4;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ins-primary-text-emphasis);
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: 999px;
}

.tl-day-date {
    font-weight: 600;
    letter-spacing: .02em;
    color: var(--ins-secondary-color);
    font-variant-numeric: tabular-nums;
}

.tl-items {
    margin: 0;
    padding: 0;
    list-style: none;
}

.tl-item {
    position: relative;
    display: grid;
    grid-template-columns: 2rem minmax(0, 1fr);
    column-gap: .75rem;
    padding-bottom: .875rem;
}

.tl-marker {
    position: relative;
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    margin-top: .3125rem;
    border-radius: 50%;
    color: var(--tl-color);
    background: color-mix(in srgb, var(--tl-color) 14%, var(--ins-secondary-bg));
    border: 1px solid color-mix(in srgb, var(--tl-color) 40%, var(--ins-secondary-bg));
    box-shadow: 0 0 0 3px var(--ins-secondary-bg);
}

.tl-marker svg { width: .9375rem; height: .9375rem; }

.tl-card {
    min-width: 0;
    padding: .625rem .875rem .6875rem;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius-lg);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
    transition: border-color .15s ease, box-shadow .15s ease;
}

.tl-item:hover .tl-card {
    border-color: color-mix(in srgb, var(--tl-color) 45%, var(--ins-border-color));
    box-shadow: 0 .25rem .75rem -.25rem rgba(0, 0, 0, .12);
}

.tl-card-head {
    display: flex;
    align-items: flex-start;
    gap: .625rem;
}

.tl-avatar {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    font-size: .625rem;
    font-weight: 700;
    letter-spacing: .02em;
    background: var(--aira-brand-100);
    color: var(--aira-brand-600);
}

.tl-avatar.is-system {
    background: var(--ins-tertiary-bg);
    color: var(--ins-secondary-color);
}

.tl-avatar svg { width: .875rem; height: .875rem; }

.tl-line {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem .375rem;
    padding-top: .125rem;
    font-size: .8125rem;
    line-height: 1.5;
}

.tl-actor { font-weight: 600; color: var(--ins-emphasis-color); }
.tl-verb { color: var(--ins-secondary-color); }
.tl-subject { font-weight: 600; color: var(--ins-body-color); }

.tl-count {
    padding: .0625rem .4375rem;
    font-size: .6875rem;
    font-weight: 600;
    color: var(--ins-secondary-color);
    background: var(--ins-tertiary-bg);
    border-radius: 999px;
}

.tl-line .stage-chip { font-size: .6875rem; line-height: 1.5; }

.tl-arrow {
    flex: none;
    width: .875rem;
    height: .875rem;
    color: var(--ins-secondary-color);
}

.tl-flag {
    padding: .0625rem .4375rem;
    font-size: .625rem;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--ins-danger-text-emphasis);
    background: var(--ins-danger-bg-subtle);
    border: 1px solid var(--ins-danger-border-subtle);
    border-radius: 999px;
}

.tl-time {
    flex: none;
    display: inline-flex;
    align-items: baseline;
    gap: .375rem;
    margin-left: auto;
    padding-top: .1875rem;
    font-size: .75rem;
    font-weight: 500;
    color: var(--ins-secondary-color);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.tl-time-day::after {
    content: "\00b7";
    margin-left: .375rem;
}

.tl-time-clock {
    font-weight: 600;
    color: var(--ins-body-color);
}

/* Folding. The header is the handle as well as the chevron, because it is the larger target and
   the one the eye is already on. */
.tl-item[data-tl-foldable] .tl-card-head { cursor: pointer; }

.tl-toggle {
    flex: none;
    display: inline-grid;
    place-items: center;
    width: 1.5rem;
    height: 1.5rem;
    margin: -.0625rem -.25rem 0 0;
    padding: 0;
    color: var(--ins-secondary-color);
    background: none;
    border: 0;
    border-radius: var(--ins-border-radius);
    cursor: pointer;
    transition: color .15s ease, background-color .15s ease;
}

.tl-toggle:hover { color: var(--ins-emphasis-color); background: var(--ins-tertiary-bg); }
.tl-toggle:focus-visible { outline: 2px solid var(--ins-primary); outline-offset: 1px; }
.tl-toggle svg { width: 1rem; height: 1rem; transition: transform .2s ease; }
.tl-item.is-collapsed .tl-toggle svg { transform: rotate(-90deg); }

/* 1fr to 0fr animates the height of content nobody measured. Visibility follows the fold, late on
   the way closed and at once on the way open, so a folded entry's "Show more" leaves the tab order.
   Overflow is clipped only while folded or folding - clipped always, it ate the focus ring of the
   last control in the body. */
.tl-collapsible {
    display: grid;
    /* minmax(0, 1fr), not the implicit auto column: a grid track grows to its widest unbreakable
       content, and a long file name then pushed the whole card past a phone's edge instead of
       ellipsing. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: 1fr;
    transition: grid-template-rows .2s ease, visibility 0s;
}

.tl-collapsible-inner { min-width: 0; min-height: 0; }

.tl-item.is-collapsed .tl-collapsible {
    grid-template-rows: 0fr;
    visibility: hidden;
    transition: grid-template-rows .2s ease, visibility 0s linear .2s;
}

.tl-item.is-collapsed .tl-collapsible-inner,
.tl-item.is-folding .tl-collapsible-inner { overflow: hidden; }

/* Indented by the avatar and its gap, so everything under a name lines up with the name. */
.tl-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem .375rem;
    margin: .125rem 0 0 2.375rem;
    font-size: .75rem;
    color: var(--ins-secondary-color);
}

.tl-meta svg { width: .8125rem; height: .8125rem; }
.tl-meta .stage-chip { padding: .0625rem .5rem; font-size: .6875rem; line-height: 1.4; }

.tl-body { margin: .5rem 0 0 2.375rem; }

/* Two classes deep on purpose: .tl-detail and .tl-quote reset their own margin, and at equal
   specificity the later rule won and pressed a follow-up's remark against the line above it. */
.tl .tl-body > * + * { margin-top: .5rem; }

.tl-quote,
.tl-note,
.tl-detail {
    margin: 0;
    font-size: .8125rem;
    line-height: 1.55;
    color: var(--ins-body-color);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.tl-quote {
    padding: .375rem .75rem;
    background: var(--ins-tertiary-bg);
    border-left: 3px solid color-mix(in srgb, var(--tl-color) 60%, transparent);
    border-radius: 0 var(--ins-border-radius) var(--ins-border-radius) 0;
}

/* The same amber as the Conversation tab, so a note reads as a note wherever it turns up. */
.tl-note {
    padding: .5rem .75rem;
    background: var(--ins-warning-bg-subtle);
    border: 1px solid var(--ins-warning-border-subtle);
    border-left: 3px solid var(--ins-warning);
    border-radius: var(--ins-border-radius);
}

.tl-item.is-bad .tl-detail { color: var(--ins-danger-text-emphasis); }

.tl-facts {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem 1rem;
    font-size: .75rem;
    color: var(--ins-secondary-color);
}

.tl-facts span { display: inline-flex; align-items: center; gap: .3125rem; }
.tl-facts svg { width: .8125rem; height: .8125rem; }

.tl-diff {
    display: grid;
    grid-template-columns: minmax(7rem, 11rem) minmax(0, 1fr);
    margin: 0;
    font-size: .8125rem;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    overflow: hidden;
}

.tl-diff-row { display: contents; }

.tl-diff dt,
.tl-diff dd {
    padding: .4375rem .75rem;
    border-top: 1px solid var(--ins-border-color);
}

.tl-diff-row:first-child dt,
.tl-diff-row:first-child dd { border-top: 0; }

.tl-diff dt {
    font-size: .75rem;
    font-weight: 600;
    color: var(--ins-secondary-color);
    background: var(--ins-tertiary-bg);
}

.tl-diff dd {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem .5rem;
    min-width: 0;
    margin: 0;
}

.tl-val {
    padding: .0625rem .4375rem;
    line-height: 1.5;
    border-radius: .25rem;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    text-decoration: none;
}

.tl-val.is-old {
    color: var(--ins-danger-text-emphasis);
    background: var(--ins-danger-bg-subtle);
    text-decoration: line-through;
    text-decoration-color: color-mix(in srgb, var(--ins-danger) 55%, transparent);
}

.tl-val.is-new {
    font-weight: 500;
    color: var(--ins-success-text-emphasis);
    background: var(--ins-success-bg-subtle);
}

.tl-val.is-blank {
    padding: 0;
    font-style: italic;
    color: var(--ins-secondary-color);
}

.tl-more {
    padding: 0;
    font-size: .75rem;
    font-weight: 600;
    color: var(--ins-primary-text-emphasis);
    background: none;
    border: 0;
    cursor: pointer;
}

.tl-more:hover { text-decoration: underline; }

.tl-files {
    display: flex;
    flex-direction: column;
    gap: .3125rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.tl-files li {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-width: 0;
    padding: .375rem .625rem;
    font-size: .8125rem;
    background: var(--ins-tertiary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
}

.tl-files svg { flex: none; width: .9375rem; height: .9375rem; color: var(--tl-color); }

.tl-file-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 500;
}

.tl-file-size {
    flex: none;
    margin-left: auto;
    font-size: .75rem;
    color: var(--ins-secondary-color);
    font-variant-numeric: tabular-nums;
}

.tl-item.is-bad .tl-file-name {
    color: var(--ins-secondary-color);
    text-decoration: line-through;
}

@media (max-width: 575.98px) {
    .tl-item { grid-template-columns: 1.75rem minmax(0, 1fr); column-gap: .5rem; }
    .tl::before { left: calc(.875rem - 1px); }
    .tl-marker { width: 1.75rem; height: 1.75rem; }
    .tl-meta,
    .tl-body { margin-left: 0; }
    .tl-diff { grid-template-columns: minmax(0, 1fr); }
    .tl-diff dd { padding-top: 0; border-top: 0; }

    /* Date over time: side by side they took the width the name needs. */
    .tl-time { flex-direction: column; align-items: flex-end; gap: 0; line-height: 1.35; }
    .tl-time-day::after { content: none; }
}

@media (prefers-reduced-motion: reduce) {
    .tl-card,
    .tl-filter,
    .tl-order,
    .tl-toggle,
    .tl-toggle svg,
    .tl-collapsible,
    .tl-item.is-collapsed .tl-collapsible { transition: none; }
}

/* --------------------------------------------------------------------------
   Closure cost
   -------------------------------------------------------------------------- */
.cost-hero {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem;
    padding: .875rem 1rem;
    border-radius: var(--ins-border-radius-lg);
    background: var(--ins-tertiary-bg);
    border: 1px solid var(--ins-border-color);
}

.cost-hero-label {
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    font-weight: 700;
    color: var(--ins-secondary-color);
}

.cost-hero-value {
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
}

.cost-hero-note { font-size: .75rem; color: var(--ins-secondary-color); }

.cost-hero-flag {
    margin-left: auto;
    font-size: .6875rem;
    font-weight: 700;
    padding: .25rem .6rem;
    border-radius: 999px;
}

.cost-hero-flag.is-low { background: var(--ins-success-bg-subtle); color: var(--ins-success-text-emphasis); }
.cost-hero-flag.is-mid { background: var(--ins-warning-bg-subtle); color: var(--ins-warning-text-emphasis); }
.cost-hero-flag.is-high { background: var(--ins-danger-bg-subtle); color: var(--ins-danger-text-emphasis); }

.cost-table td { font-size: .8125rem; }
.cost-table-total td { background: var(--ins-tertiary-bg); }

.cost-bar {
    height: 6px;
    border-radius: 999px;
    background: var(--ins-secondary-bg);
    overflow: hidden;
}

.cost-bar span {
    display: block;
    height: 100%;
    background: var(--ins-primary);
    border-radius: inherit;
}

/* --------------------------------------------------------------------------
   Attachments - the last section of Ticket details, and of the create form.
   Markup: Views/Tickets/_TicketAttachments.cshtml. Behaviour: app/js/aira-attachments.js.
   -------------------------------------------------------------------------- */
.attach-count {
    margin-left: auto;
    font-size: .75rem;
    color: var(--ins-secondary-color);
}

.attach-field { position: relative; }

/* The input carries the files and the refusal; the zone is what the operator sees. Kept in the
   DOM (not [hidden]) because the validation engine skips a control inside [hidden] - and the
   .invalid-feedback it inserts after the input is a sibling Bootstrap reveals off .is-invalid
   whether the input itself is displayed or not. */
.attach-input { display: none; }

.attach-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .25rem;
    min-height: 6rem;
    padding: 1rem;
    text-align: center;
    cursor: pointer;
    border: 1.5px dashed var(--ins-border-color);
    border-radius: var(--ins-border-radius-lg);
    background: var(--ins-tertiary-bg);
    transition: border-color .14s, background-color .14s, box-shadow .14s;
}

.attach-drop-idle {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .25rem;
}

.attach-drop:hover,
.attach-drop.is-over {
    border-color: var(--ins-primary);
    background: color-mix(in srgb, var(--ins-primary) 5%, var(--ins-tertiary-bg));
}

.attach-drop:focus-visible {
    outline: none;
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--ins-primary) 22%, transparent);
}

.attach-drop-icon svg { width: 1.5rem; height: 1.5rem; color: var(--ins-secondary-color); }
.attach-drop.is-over .attach-drop-icon svg { color: var(--ins-primary); }
.attach-drop-text { font-size: .8125rem; }

.attach-drop-link {
    font-weight: 600;
    color: var(--ins-primary-text-emphasis);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.attach-drop-hint { font-size: .75rem; color: var(--ins-secondary-color); }

.attach-field:has(.attach-input.is-invalid) .attach-drop {
    border-color: var(--ins-danger);
    background: color-mix(in srgb, var(--ins-danger) 4%, var(--ins-tertiary-bg));
}

/* Uploading. The zone stays where it is and becomes the progress bar, rather than a spinner
   somewhere else on a tab the operator may have scrolled. */
.attach-field.is-busy .attach-drop { cursor: progress; }
.attach-field.is-busy .attach-drop-idle { display: none; }

.attach-progress {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    width: min(100%, 22rem);
}

.attach-progress-text { font-size: .8125rem; font-weight: 600; }

.attach-progress-bar {
    display: block;
    height: 6px;
    overflow: hidden;
    border-radius: 999px;
    background: var(--ins-secondary-bg);
}

.attach-progress-bar > span {
    display: block;
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: var(--ins-primary);
    transition: width .2s ease-out;
}

/* Every byte has arrived and the server is compressing. There is no figure to show for that, so
   the bar says "working" instead of sitting at 100% looking stuck. */
.attach-progress-bar.is-indeterminate > span {
    width: 35%;
    transition: none;
    animation: attach-indeterminate 1.1s ease-in-out infinite;
}

@keyframes attach-indeterminate {
    from { transform: translateX(-100%); }
    to { transform: translateX(290%); }
}

@media (prefers-reduced-motion: reduce) {
    .attach-progress-bar.is-indeterminate > span { width: 100%; opacity: .5; animation: none; }
}

.attach-grid {
    display: grid;
    /* min(100%, ...) so a column never demands more than the pane has: at phone width the old
       minmax(15rem, 1fr) was wider than the dialog body and pushed the tiles off its edge. */
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
    gap: .625rem;
    margin-top: .75rem;
}

.attach-grid:empty { display: none; }
.ticket-section-head + .attach-grid { margin-top: 0; }

/* A tile. min-width: 0 on the tile and its body is the whole fix for a file name running out of
   the card: a flex or grid item's automatic minimum is its content, so a long name made the tile
   wider than its column instead of truncating. The markup asked for Bootstrap's `min-w-0`, which
   Bootstrap 5.3 does not have - so nothing was ever holding it in. */
.attach-tile {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-width: 0;
    padding: .5rem;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    background: var(--ins-body-bg);
    transition: border-color .14s;
}

.attach-tile:hover { border-color: color-mix(in srgb, var(--ins-primary) 40%, var(--ins-border-color)); }

.attach-tile.is-pending {
    border-style: dashed;
    background: var(--ins-tertiary-bg);
}

.attach-thumb {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    overflow: hidden;
    border-radius: var(--ins-border-radius);
    font-size: .625rem;
    font-weight: 700;
    letter-spacing: .04em;
    color: #fff;
    text-decoration: none;
    background: var(--ins-secondary-color);
}

.attach-thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.attach-thumb.is-image { background: var(--ins-secondary-bg); }

/* A video's first frame, with the play mark over it so it is not taken for a photo. */
.attach-thumb.is-video { position: relative; background: #111418; }

.attach-thumb video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}

.attach-play {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    margin: auto;
    border-radius: 50%;
    color: #fff;
    background: rgba(0, 0, 0, .6);
}

.attach-play svg { width: .625rem; height: .625rem; margin-left: 1px; fill: currentColor; }

/* The file would not load - most often a storage folder in the wrong place. Said with a mark
   rather than the browser's broken-image icon, and the viewer says the rest. */
.attach-thumb.is-broken { color: var(--ins-danger-text-emphasis); background: var(--ins-danger-bg-subtle); }
.attach-thumb.is-broken img,
.attach-thumb.is-broken video,
.attach-thumb.is-broken .attach-play { display: none; }
.attach-thumb.is-broken::after { content: "!"; font-size: 1rem; font-weight: 700; }

/* A PDF is the common case here - a customer's complaint mail, a test report - so it gets the
   colour people already associate with one. */
.attach-thumb.is-pdf { background: #d03b3b; }

.attach-body {
    flex: 1 1 auto;
    min-width: 0;
}

.attach-name {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: .8125rem;
    font-weight: 600;
    color: inherit;
    text-decoration: none;
}

a.attach-name:hover,
button.attach-name:hover { color: var(--ins-primary-text-emphasis); text-decoration: underline; }

/* A pending tile's thumbnail and name are buttons - they open the file viewer before Save (C12) -
   drawn exactly as a saved tile's links. */
button.attach-thumb {
    padding: 0;
    border: 0;
    cursor: pointer;
}

button.attach-name {
    max-width: 100%;
    padding: 0;
    border: 0;
    background: none;
    text-align: left;
}

/* One line, always. Size and date never shrink; the uploader's name gives way first and ends in
   an ellipsis, with the full name on its title. */
.attach-meta {
    display: flex;
    gap: .375rem;
    min-width: 0;
    margin-top: .125rem;
    white-space: nowrap;
    font-size: .6875rem;
    color: var(--ins-secondary-color);
}

.attach-meta > span { flex: 0 0 auto; }
.attach-meta > span + span::before { content: "\00b7"; margin-right: .375rem; }

.attach-meta > .attach-meta-by {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.attach-remove { flex: 0 0 auto; }
.attach-remove svg { width: 1rem; height: 1rem; }

/* Settings -> Attachments: one row per kind, its name on the left and the two named choices on
   the right, so the answer in force reads down one column. Markup: Views/Settings/_AttachmentsTab. */
.attach-kind-list {
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
}

.attach-kind-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .5rem 1rem;
    padding: .625rem .875rem;
}

.attach-kind-row + .attach-kind-row { border-top: 1px solid var(--ins-border-color); }

.attach-kind-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.attach-kind-row .btn-group .btn { min-width: 6.5rem; }

/* --------------------------------------------------------------------------
   The file viewer - app/js/aira-file-viewer.js (attachments.md, "The file viewer").
   Full screen and dark in both colour modes, as every photo viewer is: a picture is judged
   against black, not against the page. A stacked dialog like any other; nothing over the
   screen is blurred (hard rule 10b) - the ground is a colour.
   -------------------------------------------------------------------------- */
.aira-fv {
    --aira-fv-ground: #0d0f12;
    --aira-fv-ink: #f3f5f7;
    --aira-fv-muted: rgba(243, 245, 247, .64);
    --aira-fv-line: rgba(255, 255, 255, .1);
    --aira-fv-control: rgba(255, 255, 255, .08);
    --aira-fv-control-hover: rgba(255, 255, 255, .18);
    --aira-fv-pressed: rgba(255, 255, 255, .24);
    /* The brand is an operator's choice and some choices vanish on near-black; white does not. */
    --aira-focus-ring-color: #ffffff;
}

.aira-fv .modal-content {
    border: 0;
    border-radius: 0;
    color: var(--aira-fv-ink);
    background: var(--aira-fv-ground);
}

.aira-fv-head {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: .75rem;
    min-height: 3.5rem;
    padding: .5rem .75rem .5rem 1rem;
    border-bottom: 1px solid var(--aira-fv-line);
}

.aira-fv-count {
    flex: 0 0 auto;
    padding: .25rem .625rem;
    border-radius: 999px;
    font-size: .75rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    background: var(--aira-fv-control);
}

.aira-fv-count[hidden] { display: none; }

.aira-fv-heading {
    flex: 1 1 auto;
    min-width: 0;
}

.aira-fv-title {
    margin: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: .9375rem;
    font-weight: 600;
    color: inherit;
}

.aira-fv-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .125rem .75rem;
    margin-top: .125rem;
    font-size: .75rem;
    font-variant-numeric: tabular-nums;
    color: var(--aira-fv-muted);
}

.aira-fv-note {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    font-weight: 600;
    color: #fcd34d;
}

.aira-fv-note[hidden] { display: none; }
.aira-fv-note svg { width: .875rem; height: .875rem; }

.aira-fv-tools {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: .25rem;
}

.aira-fv-zoom {
    display: inline-flex;
    align-items: center;
    gap: .125rem;
    margin-right: .25rem;
    padding: .125rem;
    border-radius: .5rem;
    background: var(--aira-fv-control);
}

.aira-fv-zoom[hidden] { display: none; }

.aira-fv-tool {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .375rem;
    min-width: 2.25rem;
    height: 2.25rem;
    padding: 0 .5rem;
    border: 0;
    border-radius: .5rem;
    font-size: .8125rem;
    font-weight: 600;
    color: var(--aira-fv-ink);
    text-decoration: none;
    background: transparent;
    cursor: pointer;
}

.aira-fv-tool:hover { color: var(--aira-fv-ink); background: var(--aira-fv-control-hover); }
.aira-fv-tool[hidden] { display: none; }
.aira-fv-tool svg { width: 1.125rem; height: 1.125rem; }
.aira-fv-tool[aria-pressed="true"] { background: var(--aira-fv-pressed); }
.aira-fv-tool.is-text { min-width: 2.75rem; font-variant-numeric: tabular-nums; }
.aira-fv-tool.is-refused[aria-disabled="true"],
.aira-fv-tool.is-refused[aria-disabled="true"]:hover { opacity: .38; background: transparent; }

.aira-fv-body {
    position: relative;
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
}

/* touch-action none: every gesture on the stage is ours - a swipe, a pinch, a pull - and the
   browser's own panning would scroll the page under the dialog instead. */
.aira-fv-stage {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
}

.aira-fv-stage.is-zoomed { cursor: grab; }
.aira-fv-stage.is-panning { cursor: grabbing; }

.aira-fv-slide {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem 4.5rem;
}

.aira-fv-stage.is-frame .aira-fv-slide { padding: .75rem 4.5rem; }

.aira-fv-slide.is-enter-next { animation: aira-fv-in-next .22s ease-out; }
.aira-fv-slide.is-enter-prev { animation: aira-fv-in-prev .22s ease-out; }

@keyframes aira-fv-in-next {
    from { opacity: 0; transform: translateX(3rem); }
    to { opacity: 1; transform: none; }
}

@keyframes aira-fv-in-prev {
    from { opacity: 0; transform: translateX(-3rem); }
    to { opacity: 1; transform: none; }
}

/* Drawn at an explicit size by the script - crisp at every zoom, which a scale() transform on a
   fitted image is not - and centred on the stage, so a photo larger than it can be dragged to
   any of its four edges. Hidden until it has been measured, or it flashes at its full size. */
.aira-fv-img {
    position: absolute;
    top: 50%;
    left: 50%;
    max-width: none;
    opacity: 0;
    transform: translate(-50%, -50%);
}

.aira-fv-img.is-sized { opacity: 1; }

.aira-fv-video {
    display: block;
    max-width: 100%;
    max-height: 100%;
    border-radius: .25rem;
    background: #000;
}

.aira-fv-frame {
    width: 100%;
    height: 100%;
    border: 0;
    border-radius: .25rem;
    background: #fff;
}

.aira-fv-loading {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--aira-fv-muted);
    pointer-events: none;
}

.aira-fv-unshowable {
    max-width: 34rem;
    margin: 0;
    padding: 1rem;
    text-align: center;
    color: var(--aira-fv-muted);
}

.aira-fv-doc {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .75rem;
    max-width: 20rem;
    padding: 1.75rem 1.5rem;
    border: 1px solid var(--aira-fv-line);
    border-radius: 1rem;
    text-align: center;
    background: var(--aira-fv-control);
}

.aira-fv-doc-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3.5rem;
    height: 3.5rem;
    border-radius: .875rem;
    color: #fca5a5;
    background: rgba(208, 59, 59, .2);
}

.aira-fv-doc-icon svg { width: 1.75rem; height: 1.75rem; }

.aira-fv-doc-name {
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: .8125rem;
    color: var(--aira-fv-muted);
}

/* The arrows either side. Over the picture, on a ground of their own, so they read against a
   white photo as well as a black one. */
.aira-fv-nav {
    position: absolute;
    top: 50%;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    margin-top: -1.375rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    color: #fff;
    background: rgba(0, 0, 0, .55);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .14);
    transition: background-color .14s, opacity .14s;
}

.aira-fv-nav.is-prev { left: 1rem; }
.aira-fv-nav.is-next { right: 1rem; }
.aira-fv-nav:hover { background: rgba(255, 255, 255, .22); }
.aira-fv-nav svg { width: 1.5rem; height: 1.5rem; }
.aira-fv-nav.is-refused[aria-disabled="true"],
.aira-fv-nav.is-refused[aria-disabled="true"]:hover { opacity: .28; background: rgba(0, 0, 0, .55); }

.aira-fv.is-single .aira-fv-nav,
.aira-fv.is-single .aira-fv-strip { display: none; }

/* Every file on the complaint, along the bottom - the way to jump rather than step. */
.aira-fv-strip {
    display: flex;
    flex: 0 0 auto;
    justify-content: safe center;
    gap: .625rem;
    padding: .625rem 1rem .875rem;
    overflow-x: auto;
    border-top: 1px solid var(--aira-fv-line);
    scrollbar-width: thin;
}

.aira-fv-strip:empty { display: none; }

.aira-fv-thumb {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 5.5rem;
    height: 3.75rem;
    padding: 0;
    overflow: hidden;
    border: 2px solid transparent;
    border-radius: .5rem;
    background: var(--aira-fv-control);
    opacity: .55;
    transition: opacity .14s, border-color .14s;
}

.aira-fv-thumb:hover { opacity: .85; }
.aira-fv-thumb.is-current { border-color: #fff; opacity: 1; }
.aira-fv-thumb.is-pdf { background: rgba(208, 59, 59, .55); }

.aira-fv-thumb img,
.aira-fv-thumb video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}

.aira-fv-thumb-kind {
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    color: var(--aira-fv-ink);
}

.aira-fv-thumb-play {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    margin: auto;
    border-radius: 50%;
    color: #fff;
    background: rgba(0, 0, 0, .6);
}

.aira-fv-thumb-play svg { width: .75rem; height: .75rem; margin-left: 1px; fill: currentColor; }

@media (prefers-reduced-motion: reduce) {
    .aira-fv-slide.is-enter-next,
    .aira-fv-slide.is-enter-prev { animation: none; }
}

/* A touch screen zooms with the fingers - pinch, double tap - so the buttons would only be in
   the way of the title. */
@media (pointer: coarse) {
    .aira-fv-zoom { display: none !important; }
}

@media (max-width: 575.98px) {
    .aira-fv-head { gap: .5rem; padding-left: .75rem; }
    .aira-fv-word { display: none; }
    .aira-fv-slide { padding: .5rem; }
    .aira-fv-stage.is-frame .aira-fv-slide { padding: .5rem; }
    .aira-fv-nav { width: 2.25rem; height: 2.25rem; margin-top: -1.125rem; }
    .aira-fv-nav.is-prev { left: .375rem; }
    .aira-fv-nav.is-next { right: .375rem; }
    .aira-fv-strip { gap: .5rem; padding: .5rem .5rem .75rem; }
    .aira-fv-thumb { width: 3.5rem; height: 2.625rem; }
}

/* --------------------------------------------------------------------------
   Follow-ups
   -------------------------------------------------------------------------- */
.activity-list { display: flex; flex-direction: column; gap: .5rem; }

.activity {
    display: flex;
    align-items: flex-start;
    gap: .625rem;
    border: 1px solid var(--ins-border-color);
    border-left: 3px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    padding: .5rem .75rem;
}

.activity.is-late { border-left-color: var(--ins-danger); }
.activity.is-done { border-left-color: var(--ins-success); opacity: .75; }

.activity-mark {
    flex: 0 0 auto;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--ins-secondary-bg);
    color: var(--ins-secondary-color);
}

.activity-mark svg { width: 13px; height: 13px; }
.activity.is-late .activity-mark { background: var(--ins-danger-bg-subtle); color: var(--ins-danger); }
.activity.is-done .activity-mark { background: var(--ins-success-bg-subtle); color: var(--ins-success); }

/* --------------------------------------------------------------------------
   Marketing confirmation
   -------------------------------------------------------------------------- */
.confirm-card {
    display: flex;
    align-items: center;
    gap: .75rem;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    padding: .625rem .75rem;
    background: var(--ins-tertiary-bg);
}

.confirm-avatar {
    flex: 0 0 auto;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .75rem;
    font-weight: 700;
    background: var(--aira-brand-100);
    color: var(--aira-brand-600);
}

/* The six-digit entry. Same shape as the sign-in OTP so the two read as one product. */
.otp-row { display: flex; gap: .5rem; justify-content: center; margin: .75rem 0 .5rem; }

.otp-row input {
    width: 2.75rem;
    height: 3rem;
    text-align: center;
    font-size: 1.25rem;
    font-weight: 700;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    background: var(--ins-body-bg);
    color: var(--ins-emphasis-color);
}

.otp-row input:focus {
    outline: none;
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 3px var(--ins-primary-bg-subtle);
}

.otp-row input.filled { border-color: var(--ins-primary); }

.otp-msg { text-align: center; font-size: .75rem; color: var(--ins-secondary-color); }
.otp-msg.is-error { color: var(--ins-danger); font-weight: 600; }

/* Shown only while PULSE delivery is still to be built. Deliberately loud - it is a stand-in,
   not a feature, and it should look like one. */
.code-reveal {
    border: 1px dashed var(--ins-warning-border-subtle);
    background: var(--ins-warning-bg-subtle);
    border-radius: var(--ins-border-radius);
    padding: .75rem;
    text-align: center;
}

.code-reveal-label {
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    font-weight: 700;
    color: var(--ins-warning-text-emphasis);
}

.code-reveal-value {
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: .35em;
    font-variant-numeric: tabular-nums;
    margin: .25rem 0;
}

.code-reveal-note { font-size: .75rem; color: var(--ins-secondary-color); }

/* --------------------------------------------------------------------------
   The gate dialog - what is missing, and where it lives.
   -------------------------------------------------------------------------- */
.gate-groups { display: flex; flex-direction: column; gap: .5rem; }

.gate-group {
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    padding: .5rem .75rem;
}

.gate-group-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .5rem;
    font-size: .8125rem;
    font-weight: 700;
}

.gate-group-owner {
    font-size: .6875rem;
    font-weight: 500;
    color: var(--ins-secondary-color);
}

.gate-group-fields { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .375rem; }

.gate-field {
    font-size: .6875rem;
    padding: .1rem .45rem;
    border-radius: 999px;
    background: var(--ins-warning-bg-subtle);
    color: var(--ins-warning-text-emphasis);
    border: 1px solid var(--ins-warning-border-subtle);
}

/* --------------------------------------------------------------------------
   Track My Complaints
   -------------------------------------------------------------------------- */
/* The whole row opens the complaint on the register and on Track, exactly as it does in the
   drill dialog. Without the cursor the target is invisible - the row reacts to a click nothing
   invited, which reads as the table having done something by accident. */
#ticket-table tbody tr,
#track-table tbody tr { cursor: pointer; }

/* The stage rail beside the table.

   It is a FILTER, not a menu: each row writes the Stage control on the bar above the table.
   So the selected row has to say "this is what the table is showing" - and it does that in the
   stage's own colour, at the chip's own strength, with the colour repeated down the leading
   edge. It used to be a saturated brand fill, which made the one row the operator had chosen
   the loudest object on an otherwise quiet page and said nothing about which stage it was. */
.track-stages .list-group-item {
    display: flex;
    align-items: center;
    gap: .5rem;
    border: 0;
    border-radius: var(--ins-border-radius);
    font-size: .8125rem;
    padding: .5rem .625rem;
    color: var(--aira-ink);
}

/* Beside a long queue the rail would otherwise scroll away from the table it filters. Only at
   xl, where the two sit side by side; below that they stack and a sticky card would follow the
   reader down the page for no reason. The offset is the theme's own topbar, which is sticky. */
@media (min-width: 1200px) {
    .track-rail-card { position: sticky; top: calc(var(--ins-topbar-height) + 1rem); }
}

/* No --stage-color on the "All stages" row, so its dot draws nothing and only holds the space. */
.track-stages .kanban-column-dot { background: var(--stage-color, transparent); }

.track-stages .track-stage-name {
    flex: 1 1 auto;
    min-width: 0;
    text-align: left;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The counts are a column, not eleven badges ranged against the right edge at whatever width
   their own digits happen to be. */
.track-stages .track-stage-count {
    flex: 0 0 auto;
    min-width: 2.25rem;
    background: var(--ins-tertiary-bg);
    color: var(--aira-ink-2);
    font-weight: 600;
}

/* "Everything" is a summary of the ten rows below it, not an eleventh one of them. */
.track-stages .track-stage-all {
    margin-bottom: .25rem;
    border-bottom: 1px solid var(--aira-line);
    border-radius: var(--ins-border-radius) var(--ins-border-radius) 0 0;
}

.track-stages .list-group-item:hover { background: var(--ins-tertiary-bg); color: var(--aira-ink); }

.track-stages .list-group-item.active {
    background: color-mix(in srgb, var(--stage-color, var(--ins-primary)) 12%, transparent);
    color: var(--aira-ink);
    font-weight: 600;
    box-shadow: inset 2px 0 0 var(--stage-color, var(--ins-primary));
}

.track-stages .list-group-item.active .track-stage-count {
    background: var(--ins-secondary-bg);
    color: var(--aira-ink);
}

/* A stage holding none of this person's complaints stays on the rail - it must not jump about
   as complaints move - but it is not something to look at, and ten pills reading 0 were the
   busiest thing in the card. */
.track-stages .list-group-item.is-empty .track-stage-name { color: var(--aira-muted); }
.track-stages .list-group-item.is-empty .track-stage-count { background: transparent; color: var(--aira-muted); }
.track-stages .list-group-item.is-empty.active .track-stage-name { color: var(--aira-ink); }

/* --------------------------------------------------------------------------
   Conversation - messages and internal log notes.
   -------------------------------------------------------------------------- */
.msg-thread { display: flex; flex-direction: column; gap: .75rem; }

.msg { display: flex; gap: .625rem; align-items: flex-start; }

.msg-avatar {
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .6875rem;
    font-weight: 700;
    background: var(--aira-brand-100);
    color: var(--aira-brand-600);
}

.msg-body {
    flex: 1 1 auto;
    min-width: 0;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    padding: .5rem .75rem;
    background: var(--ins-body-bg);
}

/* A log note has to be unmistakable at a glance. Somebody writing internal working into what
   they think is a customer-facing message is the one mistake this tab can cause. */
.msg.is-note .msg-avatar { background: var(--ins-warning-bg-subtle); color: var(--ins-warning-text-emphasis); }

.msg.is-note .msg-body {
    background: var(--ins-warning-bg-subtle);
    border-color: var(--ins-warning-border-subtle);
    border-left: 3px solid var(--ins-warning);
}

.msg-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .375rem;
    font-size: .8125rem;
    margin-bottom: .25rem;
}

.msg-tag {
    font-size: .625rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    padding: .05rem .35rem;
    border-radius: 999px;
    background: var(--ins-secondary-bg);
    color: var(--ins-secondary-color);
}

.msg-tag.is-note { background: var(--ins-warning); color: #fff; }

.msg-text {
    margin: 0;
    font-size: .8125rem;
    line-height: 1.5;
    /* Preserves the line breaks somebody typed without letting a long serial number
       stretch the dialog sideways. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ------------------------------------------------------------------
   View as - an administrator previewing another role.

   The topbar control is now the whole of it: the page-wide amber band that used to
   restate it was removed as noise. Amber on the control while a preview is running,
   colour-mixed against --ins-* rather than hard-coded so it survives dark mode
   without a second rule.
   ------------------------------------------------------------------ */
.aira-viewas .topbar-link.is-previewing {
    color: #b57a00;
    background: color-mix(in srgb, #eda100 16%, transparent);
    border-radius: 6px;
}

.aira-viewas-menu {
    min-width: 16rem;
    max-height: 26rem;
    overflow-y: auto;
}

/* Each entry is its own <form> - a GET that changed the permissions the next page
   renders under could be fired by any image tag - so the buttons have to be made to
   look like the dropdown items they replace. */
.aira-viewas-menu form { margin: 0; }
.aira-viewas-menu .dropdown-item { width: 100%; text-align: left; border: 0; background: none; }

/* A section of the create form that exists for context and is filled by somebody else.
   Its inputs are genuinely disabled, so this only has to make that legible rather than
   simulate it. */
.ticket-section-form.is-locked {
    opacity: 0.72;
    /* The cursor is the fastest way to answer "why can I not click this?" before
       anybody reads the sentence above it. */
    cursor: not-allowed;
}

/* --------------------------------------------------------------------------
   The dashboard.

   Tiles, chart cards, the drill dialog and the two derived tables. Sizes come
   from the approved design rather than from a Bootstrap utility, for the same
   reason the control metrics do: a KPI figure set with `fs-4` lands wherever
   the theme's scale happens to put it.
   -------------------------------------------------------------------------- */

/* Auto-fit rather than a fixed column count. The tile set is role-scoped - a
   Customer Care user gets four tiles where an administrator gets ten - and a
   twelve-column grid would leave one of them stranded on a row of its own. */
.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: .75rem;
}

.kpi-card {
    position: relative;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius-lg, .5rem);
    box-shadow: var(--aira-shadow-1);
    padding: .875rem .9375rem .75rem;
    overflow: hidden;
    transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}

/* The stage's own colour as a spine down the left edge, which is what ties a
   tile to its board column without tinting the whole card. */
.kpi-card::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--kc, var(--ins-primary));
}

.kpi-card[data-drill] { cursor: pointer; }

.kpi-card[data-drill]:hover,
.kpi-card[data-drill]:focus-visible {
    box-shadow: var(--aira-shadow-2);
    border-color: color-mix(in srgb, var(--kc, var(--ins-primary)) 45%, var(--ins-border-color));
    transform: translateY(-1px);
    outline: none;
}

.kpi-label {
    display: flex;
    align-items: center;
    gap: .4375rem;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--aira-ink-2);
    margin-bottom: .375rem;
}

.kpi-dot {
    width: 8px;
    height: 8px;
    border-radius: 2px;
    background: var(--kc, var(--ins-primary));
    flex: 0 0 8px;
}

.kpi-value {
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--aira-ink);
    font-variant-numeric: tabular-nums;
}

/* The ageing split. It is part of the tile, not decoration: a count of 40 with
   30 of them over a month old is a different situation from 40 raised today,
   and the tile that hides that is the one nobody can act on. */
.kpi-age {
    display: flex;
    gap: .75rem;
    flex-wrap: wrap;
    margin-top: .5625rem;
    padding-top: .5rem;
    border-top: 1px solid var(--ins-border-color);
    font-size: .6875rem;
    color: var(--aira-muted);
}

.kpi-age b {
    color: var(--aira-ink-2);
    font-variant-numeric: tabular-nums;
}

/* The SLA card's three figures are three populations, so each opens its own - the
   card behind them opens all three. They have to look like the separate targets
   they are, or the only discoverable one is the card. */
.kpi-age span[data-drill] {
    cursor: pointer;
    border-radius: 3px;
    padding: 0 .1875rem;
    margin: 0 -.1875rem;
}

.kpi-age span[data-drill]:hover,
.kpi-age span[data-drill]:focus-visible {
    background: color-mix(in srgb, var(--kc, var(--ins-primary)) 12%, transparent);
    color: var(--aira-ink);
    outline: none;
}

.kpi-age span[data-drill]:hover b,
.kpi-age span[data-drill]:focus-visible b { color: var(--aira-ink); }

.kpi-age-plain { color: var(--aira-ink-2); }

/* The cost strip reads as money, so its figures are a step down in size - a
   nine-digit rupee total at the count size wraps and breaks the row. */
.kpi-grid-cost .kpi-value { font-size: 1.375rem; }

.apex-charts { min-height: 60px; }

/* The department load bar. */
.dept-load-track {
    height: 7px;
    border-radius: 4px;
    background: var(--ins-tertiary-bg);
    overflow: hidden;
}

.dept-load-fill {
    height: 100%;
    border-radius: 4px;
    transition: width .2s ease;
}

[data-dash-departments] tr[data-drill] { cursor: pointer; }

/* A figure with nothing behind it inside a mark that opens - a tile's empty age bucket, an empty
   SLA state, a department's dash. It opens nothing, the card or row around it included, so it
   does not invite the click either. A dash's title says why. */
[data-drill-none] { cursor: default; }

/* The two derived tables on the page.

   `table-sm` alone put six columns and a load bar into a card at the theme's tightest
   cell padding, which reads as a dump of figures rather than as a table. The min-width
   is what makes the wrapper scroll instead of crushing "Complaint Department" to one
   word a line - a column that wraps mid-name is how a department stops being scannable. */
.dash-table { min-width: 34rem; }

.dash-table > thead > tr > th {
    padding: .5rem .75rem;
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--aira-ink-2);
    white-space: nowrap;
}

.dash-table > tbody > tr > td {
    padding: .5625rem .75rem;
    font-variant-numeric: tabular-nums;
}

/* The name column is the only one allowed to wrap; every other cell is a figure, and a
   figure that wraps has been misread once already. The name is the SECOND cell - Sr No
   leads the row - so this selector is positional and has to move when the row does. Left
   on :first-child it silently hands nowrap to the department name, min-width stops helping
   and the card scrolls sideways instead of wrapping. */
.dash-table > tbody > tr > td:not(:nth-child(2)),
.dash-table > thead > tr > th:not(:nth-child(2)) { white-space: nowrap; }

.pivot-total td { background: var(--ins-tertiary-bg); }

/* A figure that opens the complaints behind it.

   THE AT-REST SIGNAL IS THE POINT. This was `cursor: pointer` plus a colour change on
   hover, and a colour change is invisible on a cell the pivot has already tinted with a
   colour-mix shade - so the grid read as dead, and a cell that could not open looked
   exactly like one that could. A dotted underline says "this opens" before the pointer
   arrives, which is the question the operator actually has. */
.drill-cell {
    cursor: pointer;
    font-variant-numeric: tabular-nums;
    text-decoration: underline dotted;
    text-decoration-color: color-mix(in srgb, var(--ins-primary) 32%, transparent);
    text-underline-offset: 3px;
}

/* An INSET ring rather than `outline`: the grid lives in a scroll box, and an outline is
   painted outside the cell where the box clips it away. */
.drill-cell:hover,
.drill-cell:focus-visible {
    color: var(--ins-primary);
    text-decoration-color: currentColor;
    outline: none;
    box-shadow: inset 0 0 0 2px var(--ins-primary);
}

/* A figure that genuinely cannot be listed exactly. Not greyed - it is a real number,
   and dimming it would read as missing data. It simply does not invite the click. */
.pv-locked { cursor: default; }

/* --------------------------------------------------------------------------
   The drill dialog
   -------------------------------------------------------------------------- */

.drill-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: .625rem;
    margin-bottom: 1rem;
}

.drill-stat {
    background: var(--ins-tertiary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    padding: .625rem .75rem;
}

.drill-stat-l {
    font-size: .6875rem;
    font-weight: 600;
    color: var(--aira-muted);
    text-transform: uppercase;
    letter-spacing: .02em;
}

.drill-stat-v {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--aira-ink);
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
}

.drill-stat-s {
    font-size: .6875rem;
    color: var(--aira-muted);
}

/* A figure that opens the complaints behind it. The lift is the same one .report-card
   uses, so "this is clickable" reads the same across the page - and the outline is the
   theme's own focus ring, because these are reached by Tab as well as by mouse. */
.drill-stat.is-drillable {
    cursor: pointer;
    transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}

.drill-stat.is-drillable:hover,
.drill-stat.is-drillable:focus-visible {
    border-color: var(--ins-primary);
    box-shadow: 0 2px 8px rgba(0, 0, 0, .07);
    transform: translateY(-1px);
    outline: none;
}

/* --------------------------------------------------------------------------
   A stacked dialog's own backdrop.

   app/js/aira-modals.js puts it above the dialog underneath, which is what dims that
   dialog - two dialogs at the theme's single z-index are equally bright and nothing on
   screen says which of them is live. What it must NOT do is repeat the first backdrop at
   full strength: the theme's is 0.7, and two of those put the page behind 91% black - which
   is what a report with one complaint open looked like.

   Lighter, and like every backdrop, never blurred: a backdrop-filter over the whole screen
   is what made a dialog blink while its table scrolled (scss/components/_modal.scss).
   -------------------------------------------------------------------------- */
.modal-backdrop.aira-backdrop-stacked {
    --ins-backdrop-opacity: .42;
}

/* 90% of the viewport. The dialog carries the register's own ten columns, and at
   modal-xl they wrapped two words to a line - the operator was reading a squeezed copy
   of a table that renders properly one click away. */
.drill-dialog {
    width: 90vw;
    max-width: 90vw;
    margin-left: auto;
    margin-right: auto;
}

@media (max-width: 767.98px) {
    /* Bootstrap's own dialog below md: 90vw of a phone is a ten-column table squeezed
       into 90% of nothing, and the horizontal scroll is the readable answer there. */
    .drill-dialog {
        width: auto;
        max-width: none;
    }

    /* Two frozen columns on a 400px screen leave about nothing for the third, so the
       table scrolls as one. The header still freezes - that one costs no width. */
    .aira-tscroll > table > * > tr > :nth-child(1),
    .aira-tscroll > table > * > tr > :nth-child(2) { position: static; }

    .aira-tscroll .w-wide { max-width: 14rem; }
}

/* ONE VERTICAL SCROLLBAR, AND IT BELONGS TO THE TABLE.

   The dialog used to have two. The body scrolled because Bootstrap gives
   .modal-dialog-scrollable .modal-body `overflow-y: auto`, and the table box scrolled
   because it was capped at 58vh - so a report opened with a scrollbar down the dialog and
   a second one down the rows four pixels to its left, and reading the last column of the
   last row meant dragging both. That is not a cosmetic complaint: the figures, the filter
   bar and the pager all rode the outer scroll, so the operator paged a table whose pager
   had scrolled off the bottom and whose totals had scrolled off the top.

   The whole body is now a flex column that hands its height down instead of growing to fit
   the rows: the head and the footnote take what they need, the table box takes the rest and
   is the only thing that scrolls. There is no 58vh guess anywhere - the height comes from
   what the dialog actually has left, so the rows fill the screen on a large monitor and
   still leave the pager reachable on a laptop.

   The body keeps `overflow-y: auto` as a floor, not as the plan: below about 500px of
   viewport the head alone is taller than the dialog, and clipping the filter bar would be
   worse than a scrollbar nobody reaches.

   `overflow-x: hidden` stays for the reason it was added - a fifty-eight column report
   otherwise makes the BODY the horizontal scrollport (overflow-y on one axis computes the
   other to `auto`), and the figures and the filter bar slide off to the left. Sideways
   scrolling belongs to the table box and nothing else. */
.drill-dialog .modal-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow-x: hidden;
}

/* The figures and the filter bar: what the operator opened the dialog to read, so they are
   fixed above the rows rather than scrolled with them.

   Plain, not sticky. Sticky was how this held still while the body scrolled; the body no
   longer scrolls, so pinning it to an axis that does not move is dead weight - and the
   negative margin it needed to bleed over the body's padding then had to be argued about
   every time the flex arithmetic below changed. The padding restates what the margin used
   to cancel, so it sits exactly where it did. */
.drill-head {
    flex: 0 0 auto;
    margin-bottom: .625rem;
    padding-bottom: .375rem;
    border-bottom: 1px solid var(--ins-border-color);
}

/* --------------------------------------------------------------------------
   Dialog density.

   Everything above the rows is fixed, so every pixel it takes is a pixel of table the
   operator does not get - and on a laptop the head was 334px of a 656px body, which opened a
   twenty-six row report on one and a half rows of it. Nothing is removed and nothing is
   hidden behind a toggle; the same figures and the same bar are laid out for a dialog rather
   than for a page, and the rows get what that gives back.

   Scoped to .drill-head, so the page's own cards and bars keep the approved page metrics.
   -------------------------------------------------------------------------- */

.drill-head .drill-stats {
    gap: .5rem;
    margin-bottom: .625rem;
}

.drill-head .drill-stat { padding: .4375rem .625rem; }

.drill-head .drill-stat-v { font-size: 1.0625rem; }

/* The one part of the dialog that gives up its height: everything else is measured, this
   takes what is left. `min-height: 0` and not `auto`, or a flex item refuses to shrink
   below its content and the rows push the footer off the bottom of the screen - which is
   the default, and is exactly the bug this block exists to stop. */
.drill-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* Before the rows arrive - "Building the report...", or a refusal - .drill-body holds one
   line of text. Without a floor the dialog collapses to that line and then jumps to full
   height when the answer lands. */
.drill-body > .report-empty { min-height: 8rem; }

/* Every wrapper DataTables puts between .drill-body and the scroll box, marked by
   Aira.tables.init - see app/js/aira-tables.js for why it is marked rather than matched.
   Each one has to pass the height down; a wrapper left at its default grows to fit the rows
   and the chain breaks at that link with nothing on screen saying which.

   `align-self` on the children rather than `align-items` here: the Bootstrap 5 integration
   puts `align-items-center` on the cell that holds the table, and that is an `!important`
   utility - a child's own align-self is the one thing that outranks it without writing
   another. Left alone it centres the scroll box and the table renders at its content width
   instead of the dialog's. */
.aira-tfill {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    flex: 1 1 auto;
    min-height: 0;
}

.aira-tfill > * { align-self: stretch; }

/* The row-count selector above the rows and the count/pager below them keep their own
   height; only the row that carries the table flexes. */
.aira-tfill.dt-container > * { flex: 0 0 auto; }

.aira-tfill.dt-container > .aira-tfill {
    flex: 1 1 auto;
    min-height: 0;
}

/* A SHORT SCREEN, AND THE SAME MODEL. On a 768px laptop the header, the footer, the figures
   and the filter bar have nearly spent the dialog before the rows get a line - measuring the
   head out of a 524px body left sixty pixels of table, which is one clipped row, and the body
   then overflowed anyway and brought the second scrollbar back.

   The answer is to spend less on the chrome, not to change how the dialog scrolls. Letting
   the whole body scroll again would work and was tried: it puts the table's horizontal
   scrollbar at the bottom of a two-thousand-pixel box, somewhere off the screen, and that is
   the one control a fifty-eight column report is read with. One scroll model at every height.

   So the dialog sits closer to the edges and its bands sit closer to their content, and the
   figures go to one line each - the same figures, and they still open. Roughly a hundred
   pixels, all of it handed to the rows. */
@media (max-height: 840px) {
    .drill-dialog { --ins-modal-margin: .5rem; }

    .drill-dialog .modal-content {
        --ins-modal-padding: .875rem;
        --ins-modal-header-padding: .875rem 1.25rem;
    }

    .drill-head .drill-stats {
        grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
        gap: .375rem;
        margin-bottom: .5rem;
    }

    .drill-head .drill-stat {
        display: flex;
        align-items: baseline;
        gap: .375rem;
        padding: .3125rem .5rem;
    }

    .drill-head .drill-stat-v { font-size: .9375rem; }

    .drill-head .drill-stat-s { margin-left: auto; }

    /* The floor drops with everything else, or the one box that refuses to shrink is the one
       that hands the scroll back to the dialog. */
    .drill-body .aira-tfill > .aira-tscroll { min-height: 5rem; }
}

/* The one `!important` in this block, and it answers another. The theme ships DataTables'
   own `.dt-container .dt-layout-table > div { display: block !important }`, which lands on
   the box holding the table - so the chain above reached that link and stopped dead there,
   the scroll box grew to all 2,000px of rows, and the dialog scrolled again with nothing in
   the stylesheet looking wrong. An important declaration can only be outranked by another;
   the selector is narrowed to the boxes we marked so nothing else moves. */
.dt-container .dt-layout-table > div.aira-tfill { display: flex !important; }

/* What the figure itself pinned - the pivot's open/closed scope, a narrowing the bar has
   no control for. Stated rather than offered: these decide WHICH figure was opened. */
.drill-locked {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
    margin-bottom: .5rem;
}

.drill-lock-chip {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .125rem .5rem;
    border: 1px solid color-mix(in srgb, var(--ins-primary) 35%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--ins-primary) 10%, transparent);
    color: var(--ins-primary);
    font-size: .6875rem;
    font-weight: 600;
}

/* --------------------------------------------------------------------------
   The scrolling table box, shared by both dialogs.

   Added by Aira.tables.init when a caller passes `scroll: true`. It is the one
   thing that scrolls sideways, so nothing above it can be dragged off screen.
   -------------------------------------------------------------------------- */

.aira-tscroll {
    min-height: 8rem;

    /* Bounded, so the box scrolls in BOTH directions on its own rather than handing the
       vertical one back to the dialog. That is what lets the header row stay put - a
       sticky thead only sticks inside a scrollport that actually scrolls - and it keeps
       the pager reachable instead of pushed below the fold by a hundred rows.

       58vh is the bound for a box that is NOT inside a dialog, and there is none yet. In a
       dialog the rule below replaces it with the height the dialog actually has left, which
       is the only figure that cannot disagree with the dialog it is drawn in - a guess
       either wastes a third of a large monitor or overflows a laptop and brings back the
       second scrollbar. */
    max-height: 58vh;
    overflow: auto;
    position: relative;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
}

/* In a dialog: exactly the room left under the figures and the filter bar, never a guess.

   The margins undo DataTables' own `.dt-layout-full > :only-child { margin-inline: auto }`.
   An auto margin on the cross axis cancels `stretch` outright - the box stops filling the
   dialog and takes its CONTENT width instead, which for a fifty-eight column report is seven
   thousand pixels of table with nothing clipping it. Written through .aira-tfill so the
   selector outweighs that one; it is centring a box that fills the width either way. */
.drill-body .aira-tfill > .aira-tscroll {
    flex: 1 1 auto;
    max-height: none;
    margin-inline: 0;
}

/* The header row survives the vertical scroll. An INSET box-shadow, never border-bottom:
   the theme sets border-collapse:collapse, and a collapsed border belongs to the table
   grid rather than to the cell, so it does not travel with a sticky one - the underline
   simply disappears the moment the rows move. */
.aira-tscroll > table > thead > tr > th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--ins-tertiary-bg);
    white-space: nowrap;
    box-shadow: inset 0 -1px 0 var(--ins-border-color);
}

/* The two identifying columns survive the horizontal one. Positional rather than by
   class, so both dialogs get it without marking their cells up twice - Sr No is always
   first and the complaint is always second, in the register, the drill and every report.

   At column thirty of the Excel Report the row otherwise has no identity at all: the
   complaint number has scrolled away and nothing on screen says whose observation this is. */
.aira-tscroll > table > * > tr > :nth-child(1),
.aira-tscroll > table > * > tr > :nth-child(2) {
    position: sticky;
    z-index: 2;
    background: var(--ins-secondary-bg);
}

.aira-tscroll > table > * > tr > :nth-child(1) { left: 0; }
.aira-tscroll > table > * > tr > :nth-child(2) { left: 4rem; }

/* Above the frozen columns where they cross. */
.aira-tscroll > table > thead > tr > :nth-child(1),
.aira-tscroll > table > thead > tr > :nth-child(2) { z-index: 4; }

/* The 4rem offset above is only exact if the first column really is 4rem. .sr-col sets
   width alone, which auto table layout treats as a suggestion - pinned here rather than
   on .sr-col itself, which the whole product shares. */
.aira-tscroll .sr-col {
    width: 4rem;
    min-width: 4rem;
    max-width: 4rem;
}

/* The edge, painted only while something is actually hidden behind it. */
.aira-tscroll.is-scrolled > table > * > tr > :nth-child(2)::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 6px;
    transform: translateX(100%);
    pointer-events: none;
    background: linear-gradient(to right, rgba(0, 0, 0, .13), transparent);
}

html[data-bs-theme="dark"] .aira-tscroll.is-scrolled > table > * > tr > :nth-child(2)::after {
    background: linear-gradient(to right, rgba(0, 0, 0, .55), transparent);
}

/* How wide a column may be, and whether it may wrap. Set from the column's own kind and
   declared width by pages/reports.js - never from a list of column names. */
.aira-tscroll td.cell-atomic,
.aira-tscroll th.cell-atomic { white-space: nowrap; }

.aira-tscroll td.cell-prose {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Three lines, then an ellipsis. One four-thousand-character observation otherwise sets the
   height of every row beside it and the report becomes unreadable.

   ON AN INNER DIV, NEVER ON THE CELL. `display: -webkit-box` on a <td> takes it out of table
   layout altogether - the cell stops being a cell, the row's columns stack vertically and the
   headers line up with nothing. */
.aira-tscroll .cell-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.aira-tscroll .w-narrow { max-width: 12rem; }
.aira-tscroll .w-mid { max-width: 18rem; }
.aira-tscroll .w-wide { max-width: 26rem; }

.drill-body tbody tr { cursor: pointer; }

/* DataTables' own chrome - the row-count selector above the table, the count and the
   pager below it. Both sit inside the dialog rather than on a card, so they get the
   toolbar's size rather than the page's. */
.drill-body .dt-length,
.drill-body .dt-info,
.drill-body .dt-paging { font-size: .8125rem; }

.drill-body .dt-length select { min-width: 4.5rem; }

.drill-body .dt-paging .page-link {
    padding: .25rem .5rem;
    font-size: .8125rem;
}

/* --------------------------------------------------------------------------
   The multi-select.

   See app/js/aira-multiselect.js for why this exists rather than Choices. The
   whole point of the control is that its box never changes size, so nothing
   here may make the trigger grow: the summary is one line, clipped.

   No sizes are restated. The trigger IS a .form-select.form-select-sm, so its
   height, padding and font come from the compiled control metrics in
   _aira-brand.scss and it lines up with the date inputs beside it for free.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   The searchable single-select (app/js/aira-select.js).

   Deliberately shaped like .aira-ms below it - same trigger, same panel, same
   placement maths - because the two sit side by side in every filter bar and a
   single and a multiple that look like different controls read as a bug.
   -------------------------------------------------------------------------- */
.aira-sel { position: relative; }

.aira-sel-native {
    /* Hidden by CLIP, never `display: none`. aira-validation.js treats a control it
       considers hidden as one the operator cannot fix and skips it entirely - so a
       `display: none` here would silently stop every required dropdown in the product
       being validated, with nothing on the screen to say so. Same rule, and the same
       reason, as .aira-ms-native. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    border: 0;
}

.aira-sel-trigger {
    display: flex;
    align-items: center;
    width: 100%;
    text-align: left;
    /* The theme's .form-select paints its own chevron as a background image, which is
       exactly the affordance this control wants - kept, not replaced. */
    overflow: hidden;
}

.aira-sel-summary {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--aira-ink);
}

/* Empty reads as a placeholder, not as an answer. */
.aira-sel-summary.is-empty { color: var(--ins-secondary-color); }

.aira-sel.is-open .aira-sel-trigger {
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 .15rem color-mix(in srgb, var(--ins-primary) 18%, transparent);
}

.aira-sel-panel {
    position: fixed;
    /* Opens from inside a dialog, so it has to clear the whole stack - see --aira-z-panel. */
    z-index: var(--aira-z-panel);
    display: flex;
    flex-direction: column;
    max-width: 26rem;
    padding: .5rem;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    box-shadow: var(--aira-shadow-2);
}

.aira-sel-search { padding: 0 0 .5rem; }

/* Positioned for the same reason as .aira-ms-list: highlight() in aira-select.js measures a row
   from its offsetTop, which is taken from the nearest positioned ancestor. Unpositioned, that
   was the fixed panel, and the list scrolled early or late by the search box's height. */
.aira-sel-list {
    position: relative;
    max-height: 15rem;
    overflow-y: auto;
    outline: none;
}

.aira-sel-opt {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .3125rem .4375rem;
    border-radius: var(--ins-border-radius-sm);
    font-size: .8125rem;
    color: var(--aira-ink-2);
    cursor: pointer;
}

.aira-sel-opt:hover,
.aira-sel-opt.is-active { background: var(--ins-tertiary-bg); }

.aira-sel-opt[aria-selected="true"] {
    color: var(--aira-ink);
    font-weight: 500;
    background: color-mix(in srgb, var(--ins-primary) 10%, transparent);
}

.aira-sel-text {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.aira-sel-empty,
.aira-sel-note {
    padding: .375rem .4375rem;
    font-size: .75rem;
    color: var(--aira-muted);
}

.aira-sel-note { border-top: 1px solid var(--ins-border-color); margin-top: .25rem; }

.aira-ms { position: relative; }

.aira-ms-native {
    /* Present for the form post and for read()/write(), never seen. `display: none`
       would take it out of the form in some browsers' reset paths; this keeps it a
       real control that simply occupies nothing. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    border: 0;
}

.aira-ms-trigger {
    display: flex;
    align-items: center;
    width: 100%;
    text-align: left;
    /* The theme's .form-select paints its own chevron as a background image, which
       is exactly the affordance this control wants - so it is kept, not replaced. */
    overflow: hidden;
}

.aira-ms-summary {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--aira-ink);
}

/* Empty reads as a placeholder, not as an answer. */
.aira-ms-summary.is-empty { color: var(--ins-secondary-color); }

.aira-ms.is-open .aira-ms-trigger {
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 .15rem color-mix(in srgb, var(--ins-primary) 18%, transparent);
}

/* Positioned from the trigger's rectangle by the component, because the drill
   dialog's scrolling body would clip an absolutely positioned child - and no
   z-index survives a clip. */
.aira-ms-panel {
    position: fixed;
    /* Opens from inside a dialog, so it has to clear the whole stack - see --aira-z-panel. */
    z-index: var(--aira-z-panel);
    display: flex;
    flex-direction: column;
    max-width: 22rem;
    padding: .5rem;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    box-shadow: var(--aira-shadow-2);
}

.aira-ms-search { padding: 0 0 .5rem; }

.aira-ms-list {
    max-height: 15rem;
    overflow-y: auto;
    outline: none;
}

.aira-ms-opt {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .3125rem .4375rem;
    border-radius: var(--ins-border-radius-sm);
    font-size: .8125rem;
    color: var(--aira-ink-2);
    cursor: pointer;
}

.aira-ms-opt:hover,
.aira-ms-opt.is-active { background: var(--ins-tertiary-bg); }

.aira-ms-opt[aria-selected="true"] { color: var(--aira-ink); font-weight: 500; }

.aira-ms-text {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.aira-ms-box {
    flex: 0 0 15px;
    width: 15px;
    height: 15px;
    border: 1px solid var(--aira-line-2);
    border-radius: 4px;
    background: var(--ins-secondary-bg);
    position: relative;
}

.aira-ms-opt[aria-selected="true"] .aira-ms-box {
    background: var(--ins-primary);
    border-color: var(--ins-primary);
}

.aira-ms-opt[aria-selected="true"] .aira-ms-box::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 1px;
    width: 4px;
    height: 8px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* The refusal past 200 values and the "showing the first 300" line. It is role="status", so
   it is announced when it appears rather than only seen by whoever was looking there. */
.aira-ms-empty,
.aira-ms-note,
.aira-ms-more {
    padding: .5rem .4375rem;
    font-size: .75rem;
    color: var(--aira-muted);
}

.aira-ms-note,
.aira-ms-more { border-top: 1px solid var(--ins-border-color); }

/* The server search's line (C08): "Searching all of your complaints...", or the count with Load
   more. The button (.aira-ms-act) sits in the sentence it finishes, not in a second footer. */
.aira-ms-more {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .25rem;
}

.aira-ms-more[hidden] { display: none; }

.aira-ms-foot {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-top: .375rem;
    padding-top: .5rem;
    border-top: 1px solid var(--ins-border-color);
}

.aira-ms-act {
    padding: 0;
    border: 0;
    background: none;
    font-size: .75rem;
    font-weight: 600;
    color: var(--ins-primary);
}

.aira-ms-act:hover { text-decoration: underline; }

/* Pushed to the middle so the two actions sit at the ends and the figure between
   them is read rather than clicked. */
.aira-ms-count {
    margin: 0 auto;
    font-size: .75rem;
    color: var(--aira-muted);
    font-variant-numeric: tabular-nums;
}

/* The highlight scrolls the LIST (see highlight() in aira-multiselect.js), measuring each row
   from its offsetTop - which is taken from the nearest positioned ancestor. Unpositioned, that
   was the fixed panel, and every measurement was off by the height of the search box. */
.aira-ms-list { position: relative; }

/* An <optgroup>, drawn as a heading over its options - the notification pickers' forty events
   and the Tickets page's forty-two groupings are read by section, not as one list. */
.aira-ms-group,
.aira-sel-group {
    padding: .5rem .4375rem .1875rem;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.aira-ms-group:first-child,
.aira-sel-group:first-child { padding-top: .1875rem; }

/* ==========================================================================
   The search bar - app/js/aira-search-bar.js, TagHelpers/SearchBarTagHelper.cs.

   One box holding the search text, a chip per narrowing and the Filters toggle, over a panel of
   the page's controls. Two forms: the Tickets page's master bar and the compact bar
   every other filter screen is on (filters.md).

   The box IS a .form-control.form-control-sm - the classes are on the container - so it
   inherits the compiled control metrics (rule 10a) and nothing below states its height, padding
   or font. The panel stays INSIDE the bar's root and is positioned fixed from the bar's
   rectangle: re-parented to <body>, every page driver that delegates from its root would go deaf
   to the controls it holds (filters.md T3).
   ========================================================================== */
.aira-searchbar-root {
    position: relative;
    min-width: 0;
}

.aira-searchbar {
    display: flex;
    align-items: center;
    gap: .375rem;
    min-width: 0;
    cursor: text;
}

/* The field's own focus state, carried by the container. The input inside is borderless, so its
   own :focus would say nothing - this is .form-control:focus, restated on the box: the brand
   border, the soft halo and the field ground. The one place a container mirrors a field
   (filters.md T8); FocusRingTests pins it. */
.aira-searchbar:focus-within {
    border-color: var(--ins-primary);
    box-shadow: 0 0 0 3px var(--ins-primary-bg-subtle);
    background-color: var(--ins-secondary-bg);
}

.aira-searchbar-icon {
    flex: 0 0 auto;
    width: 1em;
    height: 1em;
    color: var(--aira-muted);
}

/* One line tall, always. Chips never wrap - they scroll sideways with the scrollbar hidden
   (airacrm's rule) - and the strip gives way before the text box does, so there is always
   somewhere to type. */
.aira-searchbar-chips {
    display: flex;
    align-items: center;
    gap: .25rem;
    flex: 0 1 auto;
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
}

.aira-searchbar-chips::-webkit-scrollbar { display: none; }
.aira-searchbar-chips:empty { display: none; }

.aira-searchbar-input {
    flex: 1 1 8rem;
    min-width: 6rem;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
}

/* The box above carries the focus state, exactly as Bootstrap's own .form-control:focus drops the
   browser's outline for its border and halo. A ring inside a glowing box is two indicators for one
   field. (This is a field's outline, not the action ring rule 10c declares once.) */
.aira-searchbar-input:focus { outline: 0; }
.aira-searchbar-input::placeholder { color: var(--ins-secondary-color); }

/* type=search draws its own clear button, which would be a second x beside ours. */
.aira-searchbar-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.aira-searchbar-clear {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    padding: .125rem;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--aira-muted);
    line-height: 1;
}

.aira-searchbar-clear:hover { color: var(--aira-ink); background: var(--ins-tertiary-bg); }

/* A .btn.btn-light for its colour, radius and states - but sitting in the field's own padding
   rather than standing a button's height inside it, or the box would grow a row taller than the
   control beside it. */
.aira-searchbar-toggle {
    --ins-btn-padding-y: .0625rem;
    --ins-btn-padding-x: .5rem;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    margin-block: -.25rem;
    margin-inline-end: -.375rem;
}

.aira-searchbar-toggle[aria-expanded="true"] {
    background: var(--ins-primary-bg-subtle);
    border-color: color-mix(in srgb, var(--ins-primary) 35%, transparent);
    color: var(--ins-primary-text-emphasis);
}

.aira-searchbar-count {
    min-width: 1.25rem;
    font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   A facet chip: one per narrowing, "Label: summary" and an x that clears that facet alone.
   -------------------------------------------------------------------------- */
.aira-facet {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    max-width: 20rem;
    border: 1px solid var(--ins-border-color);
    border-radius: 999px;
    background: var(--ins-tertiary-bg);
    color: var(--aira-ink);
    font-size: .75rem;
    line-height: 1.4;
    white-space: nowrap;
}

.aira-facet-body {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    min-width: 0;
    padding: .0625rem .125rem .0625rem .5rem;
    border: 0;
    border-radius: 999px;
    background: none;
    color: inherit;
    font: inherit;
    /* Rings inside: the strip scrolls, and a ring held outside a chip is clipped by it. */
    --aira-focus-ring-offset: -2px;
}

.aira-facet-icon {
    flex: 0 0 auto;
    width: .875rem;
    height: .875rem;
    color: var(--aira-muted);
}

.aira-facet-text {
    overflow: hidden;
    text-overflow: ellipsis;
}

.aira-facet-remove {
    display: inline-flex;
    align-items: center;
    padding: .125rem .375rem .125rem .1875rem;
    border: 0;
    border-radius: 999px;
    background: none;
    color: var(--aira-muted);
    line-height: 1;
}

.aira-facet-remove:hover { color: var(--ins-danger); }

/* A figure's path is the narrowing an operator did not type: it gets the brand, so a list that
   is shorter than expected says why at a glance. */
.aira-facet--drill {
    border-color: color-mix(in srgb, var(--ins-primary) 40%, transparent);
    background: var(--ins-primary-bg-subtle);
    color: var(--ins-primary-text-emphasis);
}

.aira-facet--drill .aira-facet-icon { color: inherit; }

/* The grouping is a shape, not a narrowing - an outline, so it is never read as a filter. */
.aira-facet--groupby {
    border-style: dashed;
    background: transparent;
}

/* Kept on screen where it does not apply (the board is always grouped by stage), and dimmed. */
.aira-facet.is-dim { opacity: .55; }

/* --------------------------------------------------------------------------
   The panel. A child of the bar's root, fixed from the bar's rectangle by the script.
   -------------------------------------------------------------------------- */
.aira-search-panel {
    position: fixed;
    z-index: var(--aira-z-searchpanel);
    display: flex;
    flex-direction: column;
    max-height: 72vh;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    background: var(--ins-secondary-bg);
    box-shadow: var(--aira-shadow-2);
    overflow: hidden;
}

.aira-search-panel--compact {
    width: 22rem;
    min-width: 300px;
    max-width: calc(100vw - 1.5rem);
}

.aira-search-panel--master { width: min(1180px, 96vw); }

.aira-search-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .5rem .875rem;
    border-bottom: 1px solid var(--ins-border-color);
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.aira-search-panel-body {
    display: flex;
    flex-direction: column;
    gap: .875rem;
    min-height: 0;
    padding: .75rem .875rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.aira-search-panel-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .375rem .875rem;
    border-top: 1px solid var(--ins-border-color);
}

/* One control of the panel: an uppercase label, with a count badge while it is set, over the
   control. The label's size is the compiled .form-label's (rule 10a); only its voice is ours. */
.aira-search-section > .form-label {
    display: flex;
    align-items: center;
    gap: .375rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: 700;
    color: var(--aira-muted);
}

.aira-search-section[hidden] { display: none; }

/* The count beside a section's label while it narrows - "CUSTOMER 2" (filters.md, *The compact search bar*).
   A .badge, so its size is the badge's own; the label's tracking is for its words, not a number. */
.aira-search-section-count {
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
}

/* Two fields in one section - a From and a To - keep a gap between the first field and the
   second one's label, which would otherwise sit flush under it. */
.aira-search-section > .form-control + .form-label,
.aira-search-section > .form-select + .form-label,
.aira-search-section > .aira-sel + .form-label,
.aira-search-section > .aira-ms + .form-label { margin-top: .625rem; }

/* --------------------------------------------------------------------------
   The master panel: Filters · Group By · Refine, each column scrolling on its own.
   -------------------------------------------------------------------------- */
.aira-search-panel--master .aira-search-panel-body {
    padding: 0;
    gap: 0;
    overflow: hidden;
}

.aira-search-cols {
    display: grid;
    grid-template-columns: minmax(13rem, 1fr) minmax(13rem, 1fr) minmax(0, 2.2fr);
    flex: 1 1 auto;
    min-height: 0;
}

/* The Kanban view hides Group By (tickets-search.js, setBoard): Refine takes the freed width
   rather than leaving an empty third track. */
.aira-search-cols:has(> [data-col="groupby"][hidden]) {
    grid-template-columns: minmax(13rem, 1fr) minmax(0, 2.2fr);
}

.aira-search-col {
    min-height: 0;
    padding: .375rem 0 .75rem;
    border-inline-start: 1px solid var(--ins-border-color);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.aira-search-col:first-child { border-inline-start: 0; }

.aira-search-col-head {
    display: flex;
    align-items: center;
    gap: .375rem;
    padding: .375rem .875rem .25rem;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.aira-search-col-head svg { width: .875rem; height: .875rem; }

/* Groups of toggles, a rule between them: OR inside a group, AND across. */
.aira-search-group { padding: .25rem 0; }
.aira-search-group + .aira-search-group { border-top: 1px solid var(--ins-border-color); }

.aira-search-group-label {
    padding: .25rem .875rem 0;
    font-size: .6875rem;
    color: var(--aira-muted);
}

/* A toggle is a dropdown row with a tick: role="menuitemcheckbox", aria-checked carries the state. */
.aira-search-item {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    padding: .25rem .875rem;
    border: 0;
    background: none;
    color: var(--aira-ink-2);
    font: inherit;
    font-size: .8125rem;
    text-align: left;
    white-space: nowrap;
    --aira-focus-ring-offset: -2px;
}

.aira-search-item:hover,
.aira-search-item.is-active { background: var(--ins-tertiary-bg); color: var(--aira-ink); }

.aira-search-item:disabled,
.aira-search-item[aria-disabled="true"] {
    color: var(--ins-secondary-color);
    background: none;
    cursor: not-allowed;
}

.aira-search-tick {
    position: relative;
    flex: 0 0 15px;
    width: 15px;
    height: 15px;
    border: 1px solid var(--aira-line-2);
    border-radius: 4px;
    background: var(--ins-secondary-bg);
}

.aira-search-item[aria-checked="true"] { color: var(--aira-ink); font-weight: 500; }

.aira-search-item[aria-checked="true"] .aira-search-tick {
    background: var(--ins-primary);
    border-color: var(--ins-primary);
}

/* The same drawn tick the multi-select's rows carry - one look for "this is on" across the panel. */
.aira-search-item[aria-checked="true"] .aira-search-tick::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 1px;
    width: 4px;
    height: 8px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* A radio choice (a date period): a dot rather than a tick. */
.aira-search-item[role="menuitemradio"] .aira-search-tick { border-radius: 50%; }

.aira-search-item[role="menuitemradio"][aria-checked="true"] .aira-search-tick::after {
    left: 4px;
    top: 4px;
    width: 5px;
    height: 5px;
    border: 0;
    border-radius: 50%;
    background: #fff;
    transform: none;
}

.aira-search-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A Group By position: 1, 2, 3 in the order the groupings were ticked. */
.aira-search-order {
    flex: 0 0 auto;
    min-width: 1.125rem;
    padding: 0 .3125rem;
    border-radius: 999px;
    background: var(--ins-primary);
    color: #fff;
    font-size: .6875rem;
    font-weight: 700;
    line-height: 1.125rem;
    text-align: center;
}

/* A date field, and a date's intervals: an accordion inside its own column, so nothing ever
   flies out of the panel (Odoo 18's arrangement). The theme's accordion-button look, opened and
   closed by the script - no Bootstrap collapse, whose animation would shift the column mid-click. */
.aira-search-acc-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    padding: .3125rem .875rem;
    border: 0;
    background: none;
    color: var(--aira-ink);
    font: inherit;
    font-size: .8125rem;
    font-weight: 500;
    text-align: left;
    --aira-focus-ring-offset: -2px;
}

.aira-search-acc-head:hover { background: var(--ins-tertiary-bg); }

.aira-search-acc-head::after {
    content: "";
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    margin-inline-start: auto;
    border: solid var(--aira-muted);
    border-width: 0 1.5px 1.5px 0;
    transform: rotate(-45deg);
    transition: transform .15s ease;
}

.aira-search-acc-head[aria-expanded="true"]::after { transform: rotate(45deg); }

.aira-search-acc-value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 400;
    color: var(--ins-primary-text-emphasis);
}

.aira-search-acc-body { padding: .125rem 0 .375rem 1rem; }

/* The custom range: two dates and Apply, refused on the field when it runs backwards (rule 15). */
.aira-search-range {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .375rem;
    padding: .25rem .875rem .25rem 0;
}

.aira-search-range .aira-search-range-foot {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
}

.aira-search-custom { padding: .25rem .875rem .25rem; }

/* The Refine column: two columns of controls above xl, one below, and a field finder on top. */
.aira-refine-find { padding: .25rem .875rem .5rem; }

.aira-refine-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .75rem 1rem;
    padding: 0 .875rem;
}

@media (max-width: 1199.98px) {
    .aira-refine-grid { grid-template-columns: minmax(0, 1fr); }
}

.aira-refine-empty {
    padding: .5rem .875rem;
    font-size: .75rem;
    color: var(--aira-muted);
}

/* --------------------------------------------------------------------------
   Typing: the "Search <field> for" list under the bar.
   -------------------------------------------------------------------------- */
.aira-search-menu {
    position: fixed;
    z-index: var(--aira-z-searchpanel);
    min-width: 20rem;
    max-width: min(34rem, calc(100vw - 1.5rem));
    padding: .25rem;
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    background: var(--ins-secondary-bg);
    box-shadow: var(--aira-shadow-2);
}

.aira-search-menu-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    padding: .3125rem .5rem;
    border: 0;
    border-radius: var(--ins-border-radius-sm);
    background: none;
    color: var(--aira-ink-2);
    font: inherit;
    font-size: .8125rem;
    text-align: left;
    cursor: pointer;
}

.aira-search-menu-row.is-active { background: var(--ins-tertiary-bg); color: var(--aira-ink); }
.aira-search-menu-row strong { color: var(--aira-ink); font-weight: 600; }

/* A row with matches to pick from: an arrow says "press right". */
.aira-search-menu-row[aria-haspopup="true"]::after {
    content: "";
    width: .4375rem;
    height: .4375rem;
    margin-inline-start: auto;
    border: solid var(--aira-muted);
    border-width: 0 1.5px 1.5px 0;
    transform: rotate(-45deg);
}

.aira-search-menu-sub {
    margin: .125rem 0 .25rem 1.25rem;
    padding-inline-start: .5rem;
    border-inline-start: 2px solid var(--ins-border-color);
}

.aira-search-menu-note {
    padding: .3125rem .5rem;
    font-size: .75rem;
    color: var(--aira-muted);
}

/* --------------------------------------------------------------------------
   Below md the master panel is a full-screen sheet over an OPAQUE scrim - a colour, never a
   backdrop-filter (rule 10b: a blur over the screen blinks whatever is drawn above it) - with a
   Done button. Below sm the compact panel spans the width. The script adds these classes.
   -------------------------------------------------------------------------- */
.aira-search-scrim {
    position: fixed;
    inset: 0;
    z-index: calc(var(--aira-z-searchpanel) - 1);
    background: rgba(18, 22, 30, .5);
}

.aira-search-panel.is-sheet {
    inset: 0;
    width: auto;
    max-width: none;
    max-height: none;
    border-radius: 0;
}

/* Stacked as blocks in ONE scrolling box. Left a grid, the columns' `min-height: 0` - which is what
   lets each scroll on its own beside the others - lets the implicit rows shrink below their
   content, and the three columns were drawn on top of one another. */
.aira-search-panel.is-sheet .aira-search-cols {
    display: block;
    overflow-y: auto;
}

.aira-search-panel.is-sheet .aira-search-col {
    min-height: auto;
    border-inline-start: 0;
    border-top: 1px solid var(--ins-border-color);
    overflow: visible;
}

.aira-search-panel.is-sheet .aira-search-col:first-child { border-top: 0; }

.aira-search-panel.is-wide {
    left: .75rem;
    right: .75rem;
    width: auto;
    min-width: 0;
}

.aira-search-done { display: none; }
.aira-search-panel.is-sheet .aira-search-done { display: inline-flex; }

/* ==========================================================================
   The Tickets page - Views/Tickets/Index.cshtml, app/js/pages/tickets-workspace.js.

   Row 1: New complaint, the search bar, the view switch. Row 2: the view's
   summary - a figure the operator needs, so not an explainer band. Then the
   views, all of them rendered and one shown, chosen by `data-view` on the
   root. An inline script sets that before the first paint, so the page never
   flashes the board on its way to the List.

   The bar, the switch and the state buttons are toolbar controls, so -sm is
   their size (hard rule 10a); nothing here restates a control metric.
   ========================================================================== */

/* The page's scrollbar keeps its gutter here. The analysis views size themselves to the page, and a
   scrollbar that came and went with the chart changed the width it was drawn for, so it was drawn
   again (analysis.md T71). This page only: on a short Settings page a reserved gutter is an empty
   strip for nothing. The window is the only scroller - inside the app full screen's frame too. */
html:has([data-workspace]) { scrollbar-gutter: stable; }

/* Focus mode (app/js/aira-focus.js; analysis.md, *Full screen*): an analysis view with the whole
   screen. The shell steps aside - hidden, never covered: no overlay, no z-index, no backdrop-filter
   (hard rule 10b) - and the workspace keeps working, dialogs and toasts included, because the
   document is what is in full screen, not a card (T75). Keyed on our own attribute, never on
   data-sidenav-size (hard rule 12). */
html[data-aira-focus] .wrapper > .app-topbar,
html[data-aira-focus] .wrapper > .sidenav-menu { display: none !important; }

html[data-aira-focus] .wrapper > .content-page {
    margin-left: 0;
    min-height: 100dvh;
    /* The theme slides the page across when the sidebar changes size; here the sidebar is gone. */
    transition: none;
}

html[data-aira-focus] .content-page > .container-fluid { padding-top: .75rem; }

/* Not a place to start a complaint. */
html[data-aira-focus] .aira-ws-new { display: none; }

.aira-ws-bar,
.aira-ws-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    margin-bottom: .5rem;
}

.aira-ws-bar > .aira-searchbar-root {
    flex: 1 1 auto;
    min-width: 20rem;
}

.aira-ws-new,
.aira-ws-switch,
.aira-ws-switch-menu { flex: 0 0 auto; }

.aira-ws-switch .btn { display: inline-flex; align-items: center; }

/* Below md the switch is one dropdown naming the view, and the bar takes a row of its own. */
.aira-ws-switch-menu { display: none; }
.aira-ws-switch-label { display: none; align-items: center; gap: .375rem; }

@media (max-width: 767.98px) {
    .aira-ws-bar > .aira-searchbar-root { order: 3; flex-basis: 100%; min-width: 0; }
    .aira-ws-switch { display: none; }
    .aira-ws-switch-menu { display: block; margin-left: auto; }
}

.aira-ws-summary {
    display: none;
    margin-left: auto;
    font-size: .8125rem;
    color: var(--ins-secondary-color);
}

.aira-ws-view { display: none; }

.aira-ws[data-view="kanban"] [data-view-pane="kanban"],
.aira-ws[data-view="list"] [data-view-pane="list"],
.aira-ws[data-view="pivot"] [data-view-pane="pivot"],
.aira-ws[data-view="graph"] [data-view-pane="graph"] { display: block; }

.aira-ws[data-view="kanban"] [data-view-summary="kanban"],
.aira-ws[data-view="list"] [data-view-summary="list"],
.aira-ws[data-view="pivot"] [data-view-summary="pivot"],
.aira-ws[data-view="graph"] [data-view-summary="graph"] { display: inline; }

.aira-ws[data-view="kanban"] [data-view-label="kanban"],
.aira-ws[data-view="list"] [data-view-label="list"],
.aira-ws[data-view="pivot"] [data-view-label="pivot"],
.aira-ws[data-view="graph"] [data-view-label="graph"] { display: inline-flex; }

/* Empty and failed - the same two states in every view, above what the view could still show.
   Loading is the shared shimmer (*Loading*), never a spinner over a blank page. */
.aira-ws-state {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    margin-bottom: .75rem;
    padding: .875rem 1rem;
    font-size: .8125rem;
    color: var(--ins-secondary-color);
    background: var(--ins-tertiary-bg);
    border: 1px dashed var(--ins-border-color);
    border-radius: var(--ins-border-radius);
}

.aira-ws-state[hidden] { display: none; }

.aira-ws-state.is-failed {
    color: var(--ins-danger-text-emphasis);
    background: var(--ins-danger-bg-subtle);
    border-color: var(--ins-danger-border-subtle);
}

/* --------------------------------------------------------------------------
   The Tickets page's Pivot - app/js/pages/tickets-pivot.js, analysis.md, *The Pivot*.

   It carries the .pivot-table / .drill-cell / .pv-total /
   .pv-empty / .is-shaded contract and adds the tree: one scroll box both
   ways, the header rows and the row-header column pinned inside it. The
   table stays border-collapse: separate, so every cell owns its rules and a
   pinned cell takes them with it - under collapse a border belongs to the
   grid and stays behind while the cell travels. The frozen edges add an
   INSET box-shadow on top, so they read stronger than the grid.
   -------------------------------------------------------------------------- */
.aira-ws-toolbar { display: none; flex-wrap: wrap; align-items: center; gap: .5rem; }

.aira-ws[data-view="list"] [data-view-tools="list"],
.aira-ws[data-view="pivot"] [data-view-tools="pivot"],
.aira-ws[data-view="graph"] [data-view-tools="graph"] { display: flex; }

/* T45: the theme paints an OPEN btn-light (.show, and :active) in its tertiary colours - half-opacity
   ink on a paler ground - so a chip whose menu was open read as disabled in light mode, and did not
   change at all in dark. The toolbar's light buttons keep their ink when open and darken a step. The
   dark rule restates both: the theme's own dark .btn-light outranks the first one. */
.aira-ws-toolbar .btn-light {
    --ins-btn-active-color: var(--ins-emphasis-color);
    --ins-btn-active-bg: var(--ins-border-color);
}

html[data-bs-theme="dark"] .aira-ws-toolbar .btn-light {
    --ins-btn-active-color: var(--ins-emphasis-color);
    --ins-btn-active-bg: rgba(var(--ins-white-rgb), .18);
}

.pv-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    margin-right: .25rem;
}

.pv-strip-group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .25rem; }

.pv-strip-label {
    margin-right: .125rem;
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.pv-strip-sep { color: var(--aira-muted); }

/* Below md the strip is the Layout menu (tickets-pivot.js foldStrip gives it .dropdown-menu there):
   the three groups one under another. Stated with both classes - .pv-strip's display: flex would
   otherwise show it closed, and the theme's .dropdown-menu.show { block } would un-flex it open. */
.pv-strip.dropdown-menu:not(.show) { display: none; }

.pv-strip.dropdown-menu.show {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: .75rem;
    width: min(20rem, calc(100vw - 2rem));
    margin-right: 0;
    padding: .75rem;
}

/* Folded, each group reads as a field: its name on one line, its chips and [+] on the next. */
.pv-strip.dropdown-menu .pv-strip-group { display: flex; }
.pv-strip.dropdown-menu .pv-strip-label { flex: 0 0 100%; margin: 0 0 .125rem; }
.pv-chip { display: inline-flex; align-items: center; gap: .25rem; }
.pv-chip-caret { opacity: .6; }

/* The measures menu's rows carry the search panel's tick - one look for "this is on". */
.pv-check { display: flex; align-items: center; gap: .5rem; }
.pv-check[aria-checked="true"] .aira-search-tick { background: var(--ins-primary); border-color: var(--ins-primary); }
.pv-check[aria-checked="true"] .aira-search-tick::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 1px;
    width: 4px;
    height: 8px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.pv-card { overflow: hidden; }

.pv-scroll {
    overflow: auto;
    /* Set by the script from the box's real distance to the bottom of the window, as the board's. */
    max-height: var(--pv-max-height, 70vh);
    overscroll-behavior: contain;
    /* The table's outer frame, on the box rather than the cells, so it stays at the edge of what
       is on screen while the grid scrolls under it (C01). */
    border: var(--ins-border-width) solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
}

.pv-tree {
    width: max-content;
    min-width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    /* A ring outside a cell is clipped by the scroll box it sits in: inside, here (rule 10c). */
    --aira-focus-ring-offset: -2px;
}

/* C01: a full grid, as a table on paper and the "this table" file draw it. A right and a bottom rule
   on every cell - never .table-bordered, whose row rules are on the <tr> and paint only under
   collapse. Where a cell meets the box's frame it drops that side, so no edge is drawn twice. */
.pv-tree > thead > tr > th,
.pv-tree > tbody > tr > th,
.pv-tree > tbody > tr > td {
    border-right: var(--ins-border-width) solid var(--ins-border-color);
    border-bottom: var(--ins-border-width) solid var(--ins-border-color);
}

/* The grand total stands off the body, as the file's bold top row does. */
.pv-tree > tbody > tr.pv-total-row > * { border-bottom-width: calc(var(--ins-border-width) * 2); }

/* The right edge: every body row's last cell, the first header row's and the measure row's. The
   header rows between reach it only when there is no Total column spanning down beside them.
   `th:last-child` after the :has(), never a bare `:last-child` - see "A :has() rule ends in a tag or
   a class" in .claude/docs/ubold-ui.md; ThemeRenderingTests pins it. */
.pv-tree > tbody > tr > :last-child,
.pv-tree > thead > tr:first-child > :last-child,
.pv-tree > thead > tr.pv-measures > :last-child,
.pv-tree:not(:has(> thead th.pv-col-head.pv-total)) > thead > tr > th:last-child { border-right: 0; }

.pv-tree > tbody > tr:last-child > * { border-bottom: 0; }

/* A heading over several columns sits over the middle of them; the measure row stays right-aligned. */
.pv-tree > thead > tr > th.pv-col-head[colspan]:not([colspan="1"]) { text-align: center; }

.pv-tree > thead > tr > th {
    position: sticky;
    z-index: 2;
    background: var(--ins-tertiary-bg);
    box-shadow: none;
}

/* The frozen header's edge is under its last row, a step stronger than the grid. */
.pv-tree > thead > tr.pv-measures > th { box-shadow: inset 0 -1px 0 var(--ins-border-color); }

/* Each header row pinned under the one above it, at the offset tickets-pivot.js measured it drawn at
   (pinHeader, T85). Never an offset per row here: the rows are column levels + 2 with no ceiling, and
   a row is as tall as its content - five rows at 2.125rem apiece left a fourth level's measure row
   unpinned, sliding up over the rest, and every pinned row over the last 1.25px - the rule - of the
   one above. */
.pv-tree > thead > tr > th { top: var(--pv-top, 0px); height: 2.125rem; }

/* Above the header cells that scroll under it sideways and the row headers that scroll under it
   down. Stated at (0,2,3): the Reports-era `.pivot-table th.pv-corner { z-index: 2 }` further down
   is (0,2,2) and later, so at this file's old (0,2,2) it won, and every header scrolled over the corner. */
.pv-tree > thead > tr > th.pv-corner {
    left: 0;
    z-index: 4;
    text-align: left;
    vertical-align: bottom;
    box-shadow: inset -1px -1px 0 var(--ins-border-color);
}

.pv-tree > tbody > tr > th.pv-head-cell {
    position: sticky;
    left: 0;
    z-index: 1;
    font-weight: 400;
    text-align: left;
    white-space: nowrap;
    background: var(--ins-secondary-bg);
    box-shadow: inset -1px 0 0 var(--ins-border-color);
}

.pv-tree > tbody > tr:hover > th.pv-head-cell {
    background: color-mix(in srgb, var(--aira-brand-400) 16%, var(--ins-secondary-bg));
}

.pv-tree .pv-total-row > *,
.pv-tree .pv-total { font-weight: 700; }

.pv-tree > tbody > tr.pv-total-row > th.pv-head-cell { background: var(--ins-tertiary-bg); }

.pv-tree td.pv-value {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The column under the pointer - on a wide table, the half an eye loses (Odoo washes both). */
.pv-tree td.pv-col-hover { background: color-mix(in srgb, var(--aira-brand-400) 7%, transparent); }

.pv-toggle,
.pv-sort {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    text-align: left;
    cursor: pointer;
}

.pv-tree th.pv-head-cell .pv-toggle { padding-left: calc(var(--pv-depth, 0) * 1.25rem); }

/* No sizes here: the theme's svg.lucide { 1em } outranks a class, so an icon is the size of its text. */
.pv-toggle-icon { flex: 0 0 auto; color: var(--aira-muted); }
.pv-measure { text-align: right; }

.pv-tree tr.pv-foot > td { text-align: left; color: var(--aira-muted); font-weight: 400; }

/* A refusal the table cannot show on itself, with the way round it. */
.pv-notice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    margin-bottom: .75rem;
    padding: .625rem .875rem;
    font-size: .8125rem;
    color: var(--ins-warning-text-emphasis);
    background: var(--ins-warning-bg-subtle);
    border: 1px solid var(--ins-warning-border-subtle);
    border-radius: var(--ins-border-radius);
}

.pv-notice[hidden] { display: none; }

/* The group-by menu, "Replace with" and [+]: kept inside the pane, fixed from its anchor. */
.pv-menu {
    position: fixed;
    z-index: var(--aira-z-panel);
    min-width: 15rem;
    max-width: 22rem;
    max-height: 60vh;
    overflow: auto;
    padding: .375rem;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius);
    box-shadow: var(--aira-shadow-2);
    --aira-focus-ring-offset: -2px;
}

.pv-menu-short { min-width: 11rem; }

.pv-menu-item {
    display: block;
    width: 100%;
    padding: .375rem .625rem;
    border: 0;
    border-radius: calc(var(--ins-border-radius) - 2px);
    background: none;
    color: inherit;
    font-size: .8125rem;
    text-align: left;
}

.pv-menu-item:hover:not(.is-refused) { background: var(--ins-tertiary-bg); }
/* A grouping already taken stays listed, at half strength, with its reason under the pointer (Aira.refuse). */
.pv-menu-item.is-refused[aria-disabled="true"] { opacity: .5; }

.pv-menu-group { margin-top: .25rem; padding-top: .25rem; border-top: 1px solid var(--ins-border-color); }

.pv-menu-head {
    padding: .25rem .625rem;
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.pv-menu-line { display: flex; flex-wrap: wrap; gap: .125rem; }
.pv-menu-line .pv-menu-item { width: auto; }

/* --------------------------------------------------------------------------
   The searchable option list - Aira.optionList (app/js/aira-option-list.js),
   every grouping menu on the Tickets page (C07). The search box stays pinned;
   the list scrolls under it, inside the room Aira.groupPicker.place() measured.
   Toolbar menus, so the box is -sm (hard rule 10a).
   -------------------------------------------------------------------------- */
.pv-menu-search { display: flex; flex-direction: column; overflow: hidden; min-width: 17rem; }
.pv-menu-search .aira-ol { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.aira-ol-search { flex: 0 0 auto; padding: .125rem .125rem .375rem; }
.aira-ol-list { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; }

/* The search bar's Group By column is the scroller there, like the two columns beside it: the list
   has no height of its own, so as a scroll container it could never scroll, and its `contain` still
   stopped the wheel - the column moved only from its heading (filters.md T85). */
.aira-search-col .aira-ol-list { overflow: visible; overscroll-behavior: auto; }

.aira-ol-head {
    padding: .25rem .625rem;
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.aira-ol-group + .aira-ol-group { margin-top: .25rem; padding-top: .25rem; border-top: 1px solid var(--ins-border-color); }
.aira-ol-empty { padding: .5rem .625rem; font-size: .8125rem; color: var(--aira-muted); }

/* The option the arrows are on - the focus stays in the box, so this is its only mark. */
[data-ol-option][aria-selected="true"] { background: var(--ins-tertiary-bg); }

/* What the typed words found, in the label: the brand's tint, the ink kept. */
mark.aira-ol-hit {
    padding: 0;
    color: inherit;
    background: color-mix(in srgb, var(--aira-brand-400) 24%, transparent);
    border-radius: 2px;
}

/* "More groupings": the rest of the catalogue, folded, so the menu is no taller than it was. */
.pv-menu-more-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: .375rem .625rem;
    border: 0;
    border-radius: calc(var(--ins-border-radius) - 2px);
    background: none;
    color: inherit;
    font-size: .8125rem;
    font-weight: 600;
    text-align: left;
}

.pv-menu-more-toggle:hover { background: var(--ins-tertiary-bg); }
.pv-menu-more-count { font-size: .75rem; font-weight: 400; color: var(--aira-muted); }

/* The right-click menu: the header's actions, then its group-by list under a rule. */
.pv-menu-actions { flex: 0 0 auto; }
.pv-menu-sep { flex: 0 0 auto; margin-top: .25rem; padding-top: .5rem; border-top: 1px solid var(--ins-border-color); }
.pv-menu-context > .aira-ol-empty { flex: 0 0 auto; }


/* --------------------------------------------------------------------------
   The Tickets page's Graph - app/js/pages/tickets-graph.js, analysis.md, *The Graph*.

   The toolbar reuses the pivot's strip and chips, so a measure and an axis look
   the same in both views. The chart fills the card; a donut sits beside its own
   legend, the dashboard's .donut-legend rows, at the ring's own diameter.
   -------------------------------------------------------------------------- */
.gr-body { padding: 1rem 1.25rem .75rem; }

/* What was left off, with its figure. Not an explainer: it is empty unless something was. */
.gr-notes { margin: 0 0 .5rem; font-size: .8125rem; color: var(--aira-ink-2); }

.gr-stage { display: block; }
.gr-ring { width: 100%; min-width: 0; }

/* ApexCharts measures the ring's wrapper for its width, so for a donut the wrapper IS the
   diameter the script drew it at - a width on .apex-charts itself would be ignored (dashboard.md). */
.gr-stage.is-donut { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; }
.gr-stage.is-donut .gr-ring { flex: 0 0 var(--donut-size, 320px); max-width: var(--donut-size, 320px); }
.gr-stage.is-donut .gr-legend { flex: 1 1 240px; max-width: 440px; }

.gr-message { margin: 0; padding: 3rem 1rem; text-align: center; color: var(--aira-muted); }

/* Top N's menu heading is a sentence, not a caption: it wraps inside the menu (C05). */
.gr-top-menu { max-width: 20rem; }
.gr-top-head { white-space: normal; text-transform: none; letter-spacing: 0; font-size: .75rem; line-height: 1.4; }

.gr-foot { display: flex; justify-content: flex-end; }
.gr-table-link { display: inline-flex; align-items: center; font-size: .8125rem; }


/* --------------------------------------------------------------------------
   The grouped List - pages/ticket-list.js, ticket-screens.md, *The List*.

   Odoo's grouped list: a group is a row of its own - chevron, name, count, and
   its cost and age under the columns they belong to - and what it holds is
   indented a step under it. The complaint rows are the flat table's own cells.
   -------------------------------------------------------------------------- */
.gl-table > tbody > tr.gl-group > td {
    background: var(--aira-surface-2);
    font-weight: 600;
}

.gl-table > tbody > tr.gl-group > td.gl-head { padding-left: calc(.5rem + var(--gl-depth, 0) * 1.5rem); }

/* C02: the whole group row opens and folds, so it says so under the pointer. */
.gl-table > tbody > tr.gl-group { cursor: pointer; }
/* table-hover's wash is an inset shadow over the ground; the group's is the brand's, in its place. */
.gl-table > tbody > tr.gl-group:hover > td { --ins-table-bg-state: color-mix(in srgb, var(--aira-brand-400) 9%, transparent); }

/* As wide as the head cell, so the keyboard's ring marks the row the click acts on. Its text stays
   selectable - Chrome selects nothing inside a button otherwise, so a drag across a customer's name
   ended as a click and folded the group; selected, the row's click handler leaves it be. */
.gl-toggle {
    display: inline-flex;
    width: 100%;
    justify-content: flex-start;
    align-items: center;
    gap: .5rem;
    padding: 0;
    user-select: text;
    border: 0;
    background: none;
    color: var(--aira-ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.gl-chevron { flex: 0 0 auto; color: var(--aira-muted); transition: transform .12s ease; }
.gl-toggle[aria-expanded="true"] .gl-chevron { transform: rotate(90deg); }

.gl-count {
    padding: 0 .4rem;
    border-radius: 999px;
    font-size: .75rem;
    font-weight: 600;
    line-height: 1.5;
    color: var(--aira-ink-2);
    background: var(--ins-tertiary-bg);
    font-variant-numeric: tabular-nums;
}

.gl-agg { font-variant-numeric: tabular-nums; }
.gl-agg-note { font-size: .75rem; font-weight: 400; color: var(--aira-muted); }

/* What a group holds sits a step under it: the Sr No cell carries the indent. */
.gl-table > tbody > tr.gl-row > td:first-child,
.gl-table > tbody > tr.gl-more > td { padding-left: calc(.5rem + var(--gl-depth, 1) * 1.5rem); }

.gl-table > tbody > tr.gl-row { cursor: pointer; }

/* The List's tables scroll sideways in their own box (ticket-screens.md, *The List*). Thirteen columns come to
   1,160px - wider than a phone and than a laptop - and with nothing to scroll them the whole PAGE
   went sideways, the search bar and the view switch with it. The board already scrolls inside
   itself; now the List does too, and only the table moves: the row count and the pager stay where
   they are. Nothing in the List is sticky, so the scroll box costs it nothing. */
[data-view-pane="list"] .dt-layout-table > *,
[data-view-pane="list"] .gl {
    overflow-x: auto;
}
.gl-more-button { font-size: .8125rem; }


/* --------------------------------------------------------------------------
   Chart chrome.

   The theme's compiled _apexcharts.scss paints axis text, gridlines, markers and
   the donut's centre block from its own tokens, two of them with `!important`.
   Those rules beat anything passed in the options object, which is why a chart
   could be configured correctly and still render pale and dashed. aira.css loads
   after app.min.css, so this is where the prototype's ink actually lands.

   These are chart chrome, not control metrics - _aira-brand.scss still owns
   every field, label and button size. The card-title rule is scoped to the
   dashboard for exactly that reason.
   -------------------------------------------------------------------------- */
.apex-charts .apexcharts-gridline {
    stroke: var(--chart-grid);
    stroke-dasharray: 0;
}

.apex-charts .apexcharts-xaxis text,
.apex-charts .apexcharts-yaxis text {
    fill: var(--chart-tick);
    font-size: 11.5px;
}

/* `apexcharts-data-labels` is the group and `apexcharts-datalabel` the text node.
   `apexcharts-datalabels`, without the hyphen, is a THIRD class ApexCharts uses
   somewhere else entirely - it matches none of these, so the rule that named it
   styled nothing and the printed values kept whatever grey they landed in.

   Not inside a heatmap: there each figure is written in its band's own ink (the
   calendar ramp's --aira-heat-ink-*, as the fill attribute), and this grey - a CSS
   declaration, which always beats an attribute - put dark figures on the dark cells
   of the Tickets graph's heatmap, where they could not be read. */
.apex-charts .apexcharts-data-labels text:not(.apexcharts-heatmap-series text),
.apex-charts text.apexcharts-datalabel:not(.apexcharts-heatmap-series text) {
    fill: var(--chart-dlab);
    font-size: 12px;
    font-weight: 700;
}

.apex-charts .apexcharts-heatmap-series text.apexcharts-datalabel { font-size: 12px; font-weight: 700; }

/* The centre of a donut and the figure in the gauge. The theme pins both to
   --ins-body-color with !important, so this has to answer in kind. */
.apex-charts .apexcharts-datalabel-value {
    fill: var(--aira-ink) !important;
    letter-spacing: -.02em;
}

.apex-charts .apexcharts-datalabel-label {
    fill: var(--aira-muted) !important;
    letter-spacing: .06em;
    text-transform: uppercase;
}

#chart-sla .apexcharts-track path { stroke: var(--chart-grid); }


/* --------------------------------------------------------------------------
   The SLA gauge.

   The arc is a semicircle, so ApexCharts leaves the lower half of the SVG box
   empty and will not crop it - the reading is pulled back up into the arc by
   the ring's negative margin rather than left floating under a half-empty box.

   The figure is HTML, not an ApexCharts data label, because the theme pins
   `.apexcharts-datalabel-value` with `!important` and a 34px reading is the one
   number on this card somebody actually reads.
   -------------------------------------------------------------------------- */
.gauge-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    padding-bottom: .5rem;
}

.gauge-ring {
    /* Wide enough not to clip the ring the chart's own height decides - see gauge() in
       pages/dashboard.js. Wider than that is white space either side of it. */
    width: 300px;
    max-width: 100%;
    /* The part of the box the semicircle does not use. Without it the reading sits a
       hundred pixels below the arc it belongs to, which is how it shipped.

       Retune this whenever the arc's thickness changes: ApexCharts crops a semicircle's
       container to the arc it drew, so a thicker band is a shorter box, and the figure
       climbs into the opening of the arc rather than sitting on its baseline. */
    margin-bottom: -53px;
}

.gauge-figure {
    text-align: center;
    /* Over the empty half of the arc's box - which has no marks in it, but does
       have the SVG's own hit area. */
    position: relative;
    z-index: 1;
    pointer-events: none;
}

.gauge-big {
    font-size: 34px;
    font-weight: 700;
    letter-spacing: -.03em;
    line-height: 1;
    color: var(--aira-ink);
    font-variant-numeric: tabular-nums;
}

.gauge-cap {
    margin-top: .375rem;
    font-size: 11.5px;
    color: var(--aira-muted);
}

/* The prototype's card header: a shade smaller and a step heavier than the
   theme's, with a description that is readable rather than a whisper. */
#dashboard-page .card-header .card-title {
    font-size: 14px;
    font-weight: 650;
    letter-spacing: -.005em;
    color: var(--aira-ink);
}

/* Under the title, always. The theme's card header is a flex row, so a title and a
   caption side by side sit on ONE line until the caption is long enough to wrap - which
   made a card's header shape depend on how much its caption happened to say. Stacked is
   also the prototype's own header, and it leaves the caption the full card width. */
#dashboard-page .card-header .card-sub {
    flex: 0 0 100%;
    display: block;
    margin-top: 1px;
    font-size: 12px;
    line-height: 1.45;
    color: var(--aira-muted);
}


/* --------------------------------------------------------------------------
   The donut's legend.

   Ours rather than ApexCharts', and the reason is in donut() in
   pages/dashboard.js. The shape is the prototype's: swatch, name, count, and a
   percentage in a fixed right-hand column so the numbers line up down the card.
   -------------------------------------------------------------------------- */
.donut-wrap {
    display: flex;
    align-items: center;
    /* 1rem rather than 1.5: the legend's rows carry a name, a count and a percentage in
       columns, and the name is the one that gives way. Eight pixels of gap is eight
       pixels of stage name. */
    gap: 1rem;
    flex-wrap: wrap;
}

/* The ring is exactly the diameter its card reserved. ApexCharts measures
   el.parentNode, so --donut-size on this wrapper is the only place the number can
   be stated - a width on .apex-charts itself is a silent no-op, and a fixed width
   here ignored the variable the view was setting and drew every ring at 190. */
.donut-ring { flex: 0 0 var(--donut-size, 190px); max-width: var(--donut-size, 190px); }

.donut-legend { flex: 1 1 150px; min-width: 150px; }

.donut-leg {
    display: flex;
    align-items: center;
    gap: 9px;
    /* Bleeds into the card's own padding so the hover band reads as a row of the
       card rather than as a box floating inside it. */
    margin: 0 -8px;
    padding: 4px 8px;
    border-radius: 6px;
    font-size: 12.5px;
    line-height: 1.5;
}

.donut-leg[data-drill] { cursor: pointer; }
.donut-leg[data-drill]:hover,
.donut-leg[data-drill]:focus-visible {
    background: var(--aira-brand-50);
    outline: none;
}

/* The class names are aira-charts.js's own. They were `.n`, `.b` and `.p` here
   while the factory wrote `donut-leg-name` / `-count` / `-pct`, so the name never
   took its `flex: 1` and the row collapsed against the ring: the count and the
   percentage sat wherever the text ended instead of in columns down the card, and
   a third of every donut card was empty. Element selectors are not enough - rename
   one side and the other must follow. */
.donut-leg-dot {
    flex: 0 0 9px;
    width: 9px;
    height: 9px;
    border-radius: 2px;
}

/* Takes the slack, which is what puts the two figures on the card's right edge. */
.donut-leg-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--aira-ink-2);
}

.donut-leg-count {
    min-width: 28px;
    text-align: right;
    color: var(--aira-ink);
    font-variant-numeric: tabular-nums;
}

.donut-leg-pct {
    width: 44px;
    text-align: right;
    color: var(--aira-muted);
    font-variant-numeric: tabular-nums;
}

html[data-bs-theme="dark"] .donut-leg[data-drill]:hover,
html[data-bs-theme="dark"] .donut-leg[data-drill]:focus-visible {
    background: var(--ins-tertiary-bg);
}


/* --------------------------------------------------------------------------
   The complaint calendar.

   A month grid rather than the ApexCharts heatmap it replaces: weeks-as-columns
   is compact and unreadable, and an operator asking what happened on the 22nd
   should not have to count squares. Every colour is a --aira-heat-* token,
   because this is DOM and app.js's theme observer repaints ApexCharts instances
   and nothing else - a resolved hex here would survive a theme switch for ever.
   -------------------------------------------------------------------------- */
.aira-cal {
    /* Panels are an intrinsic width and the row wraps. Never flex: 1 - one month
       stretched across a full-width card is a grid of 34px cells in 1600px of
       dead space. */
    --aira-cal-cell: 34px;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 1rem .875rem;
}

.aira-cal-month {
    padding: .75rem;
    border: 1px solid var(--ins-border-color);
    border-radius: .625rem;
    background: var(--aira-surface-2);
}

.aira-cal-month-name {
    margin-bottom: .625rem;
    font-size: .8125rem;
    font-weight: 700;
    color: var(--aira-ink);
}

.aira-cal-grid {
    display: grid;
    grid-template-columns: repeat(7, var(--aira-cal-cell));
    gap: 3px;
}

.aira-cal-wd {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 1.5rem;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--aira-muted);
}

.aira-cal-day {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--aira-cal-cell);
    height: var(--aira-cal-cell);
    border-radius: 6px;
    font-size: .6875rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}

/* In the month, outside the window. It holds its column so the weeks under it
   stay beneath the right weekday, and shows nothing. */
.aira-cal-day.is-out { visibility: hidden; }

.aira-cal-day[data-level="0"] { background: var(--aira-heat-0); color: var(--aira-heat-ink-0); }
.aira-cal-day[data-level="1"] { background: var(--aira-heat-1); color: var(--aira-heat-ink-1); }
.aira-cal-day[data-level="2"] { background: var(--aira-heat-2); color: var(--aira-heat-ink-2); }
.aira-cal-day[data-level="3"] { background: var(--aira-heat-3); color: var(--aira-heat-ink-3); }
.aira-cal-day[data-level="4"] { background: var(--aira-heat-4); color: var(--aira-heat-ink-4); }
.aira-cal-day[data-level="5"] { background: var(--aira-heat-5); color: var(--aira-heat-ink-5); }

.aira-cal-day:hover,
.aira-cal-day:focus-visible {
    outline: 2px solid var(--aira-heat-ring);
    outline-offset: 1px;
}

/* The bubble is CSS rather than a tooltip instance per cell: eight hundred day
   cells is eight hundred initialisations, and this one survives keyboard focus,
   which the reference design needs. */
.aira-cal-day:hover::after,
.aira-cal-day:focus-visible::after {
    content: attr(data-tip);
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    padding: .25rem .4375rem;
    border-radius: 6px;
    background: var(--aira-ink);
    color: var(--ins-secondary-bg);
    font-size: 10px;
    font-weight: 500;
    white-space: nowrap;
    pointer-events: none;
}

.aira-cal-legend {
    display: flex;
    align-items: center;
    gap: .375rem;
    font-size: 11px;
    color: var(--aira-muted);
}

.aira-cal-legend i {
    width: 12px;
    height: 12px;
    border-radius: 3px;
}

.aira-cal-legend i[data-level="0"] { background: var(--aira-heat-0); box-shadow: inset 0 0 0 1px var(--ins-border-color); }
.aira-cal-legend i[data-level="1"] { background: var(--aira-heat-1); }
.aira-cal-legend i[data-level="2"] { background: var(--aira-heat-2); }
.aira-cal-legend i[data-level="3"] { background: var(--aira-heat-3); }
.aira-cal-legend i[data-level="4"] { background: var(--aira-heat-4); }
.aira-cal-legend i[data-level="5"] { background: var(--aira-heat-5); }

@media (max-width: 575.98px) {
    .aira-cal { --aira-cal-cell: 30px; }
}

/* ==========================================================================
   REPORTS

   The catalog tiles, the two analytical cards and the pivot grid.

   Every colour here is a token. A hex written in a block dark mode does not
   redefine is a colour dark mode cannot reach - the same reason --kpi-* and
   --chart-* exist rather than hexes in C#.
   ========================================================================== */

/* The five response bands, at the prototype's own hues. Declared beside the
   chart palette rather than in C# so the bar and the stat spine that share a
   band share one definition. */
:root {
    --resp-fast: #0ca30c;
    --resp-good: #1baf7a;
    --resp-fair: #eda100;
    --resp-slow: #ec835a;
    --resp-late: #d03b3b;

    /* The report tile accents, one per report, at the prototype's numbers. */
    --report-excel: #1baf7a;
    --report-sla: #2a78d6;
    --report-ageing: #eda100;
    --report-root: #4a3aa7;
    --report-caction: #eb6834;
    --report-cost: #008300;
    --report-marketing: #4a3aa7;
}

/* Lifted for dark mode, the same way --stage-* and --kpi-* are: these fill a
   whole tile badge and a whole bar, and the darker half of the set reads as two
   indistinguishable smudges on a #1b1c24 card. */
html[data-bs-theme="dark"] {
    --resp-fast: #35c035;
    --resp-good: #2fd39a;
    --resp-fair: #f5b731;
    --resp-slow: #ff9d70;
    --resp-late: #ff6f6e;

    --report-excel: #2fd39a;
    --report-sla: #529ff0;
    --report-ageing: #f5b731;
    --report-root: #8f7ee8;
    --report-caction: #ff8355;
    --report-cost: #21ab53;
    --report-marketing: #8f7ee8;
}

/* ---- The catalog ------------------------------------------------------- */

.report-grid {
    display: grid;
    /* The prototype's 268px minimum. Below it a tile's description wraps to five
       lines and the grid reads as a list. */
    grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
    gap: .875rem;
}

.report-card {
    display: block;
    width: 100%;
    text-align: left;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--ins-border-color);
    border-radius: var(--ins-border-radius-lg);
    padding: 1.0625rem;
    box-shadow: var(--aira-shadow-1);
    transition: box-shadow .16s, transform .16s, border-color .16s;
}

.report-card:hover,
.report-card:focus-visible {
    box-shadow: var(--aira-shadow-2);
    transform: translateY(-1px);
    border-color: var(--aira-brand-400);
}

.report-ic {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: grid;
    place-items: center;
    margin-bottom: .75rem;
    /* The accent at 12% behind the icon at full strength - the same recipe the
       stage chip uses, so a tile and a chip of the same hue look related. */
    background: color-mix(in srgb, var(--report-accent) 12%, transparent);
    color: var(--report-accent);
}

.report-ic svg { width: 20px; height: 20px; }

.report-card b {
    display: block;
    font-size: .875rem;
    font-weight: 650;
    margin-bottom: .25rem;
    color: var(--aira-ink);
}

.report-card p {
    margin: 0;
    font-size: .75rem;
    color: var(--aira-ink-2);
    line-height: 1.5;
}

.report-scope {
    display: block;
    margin-top: .5rem;
    font-size: .6875rem;
    color: var(--aira-muted);
    line-height: 1.45;
}

.report-foot {
    display: flex;
    align-items: center;
    gap: .375rem;
    margin-top: .75rem;
    flex-wrap: wrap;
}

.report-format {
    font-size: .625rem;
    font-weight: 700;
    letter-spacing: .04em;
    padding: .1875rem .4375rem;
    border-radius: 4px;
    background: var(--ins-tertiary-bg);
    color: var(--aira-ink-2);
}

/* How many complaints the report would cover right now. The point of the tile:
   a report whose answer is empty says so before anybody downloads it. */
.report-count {
    margin-left: auto;
    font-size: .6875rem;
    font-weight: 600;
    color: var(--aira-ink-2);
}

.report-count.is-empty { color: var(--aira-muted); font-weight: 400; }

.report-empty {
    padding: 1.25rem;
    text-align: center;
    font-size: .8125rem;
    color: var(--aira-muted);
}

/* ---- Shared bars ------------------------------------------------------- */

.share-track {
    display: block;
    height: 7px;
    border-radius: 4px;
    background: var(--ins-tertiary-bg);
    overflow: hidden;
}

.share-fill {
    display: block;
    height: 100%;
    border-radius: 4px;
    background: var(--aira-brand-400);
}

.report-bars { display: flex; flex-direction: column; gap: .4375rem; }

.report-bar {
    display: grid;
    grid-template-columns: 8.5rem 1fr 3rem 2.5rem;
    align-items: center;
    gap: .625rem;
    font-size: .75rem;

    /* Bleeds into the card's own padding so the hover band reads as a row OF the card
       rather than a box floating inside it - .donut-leg's trick, because a response band
       and a donut legend row are the same gesture. On .report-bar rather than on
       [data-drill] so a bar that ever stops opening keeps its alignment. */
    margin: 0 -.5rem;
    padding: .1875rem .5rem;
    border-radius: 6px;
}

.report-bar[data-drill] {
    cursor: pointer;
    transition: background-color .12s ease, color .12s ease;
}

/* The same band-and-tint the dashboard's own drillable rows use. A colour change on the
   label alone was too quiet to read as clickable, and there was no focus state at all -
   tabbing onto one showed a browser outline that appears nowhere else in the product. */
.report-bar[data-drill]:hover,
.report-bar[data-drill]:focus-visible {
    background: var(--aira-brand-50);
    outline: none;
}

.report-bar[data-drill]:focus-visible {
    box-shadow: inset 0 0 0 2px var(--ins-primary);
}

.report-bar[data-drill]:hover .report-bar-name,
.report-bar[data-drill]:focus-visible .report-bar-name { color: var(--ins-primary); }

/* --aira-brand-50 is a near-white, defined on bare :root only - left alone it is a
   glowing block on a dark card. The same override .donut-leg already carries. */
html[data-bs-theme="dark"] .report-bar[data-drill]:hover,
html[data-bs-theme="dark"] .report-bar[data-drill]:focus-visible {
    background: var(--ins-tertiary-bg);
}

.report-bar-name { color: var(--aira-ink-2); }
.report-bar-value { text-align: right; font-variant-numeric: tabular-nums; }
.report-bar-pct { text-align: right; color: var(--aira-muted); font-variant-numeric: tabular-nums; }

/* A band that counted nothing is still printed - it is a step of the scale - but it opens nothing,
   so it carries no data-drill and reads quieter than the bands that do. */
.report-bar.is-empty .report-bar-name,
.report-bar.is-empty .report-bar-value { color: var(--aira-muted); }
.report-bar.is-empty .report-bar-value { font-weight: 500; }

@media (max-width: 575.98px) {
    .report-bar { grid-template-columns: 1fr 3rem; }
    .report-bar .share-track { grid-column: 1 / -1; order: 3; }
}

[data-rep-marketing-rows] tr[data-drill] { cursor: pointer; }

/* ---- The pivot table, and a section's labelled bar ------------------------
   Written for the Reports cross-tab, retired in Phase 7h. The table rules are the
   Tickets page's Pivot's now (.pivot-table.pv-tree, whose tree is further up);
   the bar is the cost card's Group by. */

.pivot-bar {
    display: flex;
    gap: .625rem;
    align-items: flex-end;
    flex-wrap: wrap;
}

.pivot-field { display: flex; flex-direction: column; min-width: 9.5rem; flex: 0 1 11rem; }
.pivot-field .form-label { letter-spacing: .06em; }
.pivot-spacer { flex: 1 1 auto; }

.pivot-table { min-width: 40rem; font-variant-numeric: tabular-nums; }

.pivot-table > thead > tr > th {
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--aira-muted);
    white-space: nowrap;
    vertical-align: bottom;
}

.pivot-table > tbody > tr > td { font-size: .8125rem; }

/* The corner stays put while the grid scrolls sideways. A table thirty columns
   wide is unreadable the moment the row names leave the viewport. */
.pivot-table th.pv-corner {
    position: sticky;
    left: 0;
    z-index: 2;
    white-space: nowrap;
    background: var(--ins-secondary-bg);
}

.pivot-table th.pv-corner {
    text-transform: none;
    color: var(--aira-ink-2);
    background: var(--ins-tertiary-bg);
}

/* The link colour, the theme's primary-for-text: the same blue in light, #769ac6 in dark.
   --ins-primary stays dark blue on the dark ground - about 2:1. */
.pivot-table th.pv-corner span { color: var(--ins-link-color); font-weight: 800; }

/* The row under the pointer, so a cell thirty columns out is readable back to its name. */
.pivot-table tbody tr:hover > td {
    background: color-mix(in srgb, var(--aira-brand-400) 7%, transparent);
}

/* Beats td.is-shaded below, which is a single class and would otherwise win the tie and
   leave the hover doing nothing on exactly the cells that carry a value. */
.pivot-table td.drill-cell:hover,
.pivot-table td.drill-cell:focus-visible {
    background: color-mix(in srgb, var(--aira-brand-400) 26%, transparent);
}

.pivot-table th.pv-col-head { cursor: pointer; }

.pivot-table .pv-total { background: var(--ins-tertiary-bg); }

.pivot-table tr.pv-foot > td {
    background: var(--ins-tertiary-bg);
    border-top: 2px solid var(--aira-line-2);
}

/* A cell with nothing behind it. Deliberately blank rather than an em dash: a
   dash is a value somebody could read as zero, and a genuine zero in the next
   column along is a different fact. */
.pivot-table td.pv-empty { background: transparent; }

/* The shade. Painted as a tint of the brand at an opacity the server's value
   decided, so it survives dark mode - a fixed rgba() would be a light wash on a
   dark card. */
.pivot-table td.is-shaded {
    background: color-mix(in srgb, var(--aira-brand-400) calc(var(--pv-shade, 0) * 100%), transparent);
}

/* --------------------------------------------------------------------------
   The confirmation (Aira.confirm, SweetAlert2) over the dialog stack.

   SweetAlert2 sits at its own 1060, which is above a single dialog and BELOW a stacked one
   (1075 and up) - so "Discard your changes?" asked from a complaint opened out of a report was
   painted under the complaint it was about. See --aira-z-alert. What makes it hold the keyboard
   and the page is app/js/aira-modals.js, not this.
   -------------------------------------------------------------------------- */
.swal2-container {
    z-index: var(--aira-z-alert);
}

/* ==========================================================================
   Access & permissions - the grid in Settings > Access and in the role dialog
   (Views/Shared/_PermissionEditorBody.cshtml).

   Every group is open at once. The small module cards flow through columns, so a one-row card
   does not leave a hole beside a six-row one; the stage tiles and the report matrix span the
   width. A granted row is tinted in its card's accent, so what is on reads at a glance rather
   than by hunting for ticks. Every colour is a token with a dark value, and the accent is mixed
   against `transparent` rather than white so the tint sits on either theme.
   ========================================================================== */
.modal-dialog.aira-modal-wide {
    max-width: min(1720px, calc(100vw - 2.5rem));
}

.perm-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: .5rem 1.5rem;
    margin-bottom: 1rem;
}

.perm-subject-name {
    margin: 0 0 2px;
    font-size: 15px;
    font-weight: 700;
    color: var(--aira-ink);
}

.perm-subject-role {
    font-size: 13px;
    color: var(--aira-ink-2);
}

.perm-summary {
    display: inline-flex;
    align-items: center;
    gap: .625rem;
    margin-left: auto;
    font-size: 13px;
    color: var(--aira-ink-2);
}

.perm-summary strong {
    font-weight: 700;
    color: var(--aira-ink);
    font-variant-numeric: tabular-nums;
}

.perm-summary .badge svg { width: 12px; height: 12px; vertical-align: -2px; }

.perm-board {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.perm-card--configuration {
    container: perm-config / inline-size;
    min-width: 0;
}

.master-permission-panel {
    border-top: 1px solid var(--aira-line);
}

.master-permission-heading {
    padding: .75rem 1rem .5rem;
}

.master-permission-heading h6 {
    font-size: 13px;
    font-weight: 700;
    color: var(--aira-ink);
}

/* The three global rights, as one segmented row: they are a set, read left to right. */
.master-global-rights {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: .5rem;
    padding: 0 1rem .875rem;
    border-bottom: 1px solid var(--aira-line);
}

.master-global-right {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    min-width: 0;
    margin: 0;
    padding: .5rem .75rem;
    border: 1px solid var(--aira-line);
    border-radius: var(--ins-border-radius);
    background: var(--ins-secondary-bg);
    color: var(--aira-ink);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color .12s ease, border-color .12s ease;
}

.master-global-right:hover { background: var(--aira-surface-2); }

.master-global-right:has(.perm-check:checked) {
    font-weight: 600;
    border-color: color-mix(in srgb, var(--perm-accent) 45%, var(--aira-line));
    background: color-mix(in srgb, var(--perm-accent) 7%, transparent);
}

.master-global-right .form-check-input {
    margin: 0;
}

.master-show-heading {
    padding: .625rem 1rem .375rem;
    color: var(--aira-muted);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    background: var(--aira-surface-2);
    border-bottom: 1px solid var(--aira-line);
}

/* Columns from the card's own width. Two columns in a narrow lane, with a long name wrapping onto a
   second line, beat one: a single column made Configuration a thousand pixels tall at 1440 wide. */
.master-show-list {
    display: grid;
    grid-template-columns: 1fr;
}

@container perm-config (min-width: 320px) {
    .master-show-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@container perm-config (min-width: 760px) {
    .master-show-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.master-show-row {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-width: 0;
    min-height: 38px;
    margin: 0;
    padding: .5rem 1rem;
    border-bottom: 1px solid var(--aira-line);
    color: var(--aira-ink);
    cursor: pointer;
    transition: background-color .12s ease;
}

.master-show-row:hover { background: var(--aira-surface-2); }

.master-show-row:has(.master-show-check:checked) {
    background: color-mix(in srgb, var(--perm-accent) 7%, transparent);
}

.master-show-row:has(.master-show-check:checked):hover {
    background: color-mix(in srgb, var(--perm-accent) 11%, transparent);
}

.master-show-row .form-check-input {
    flex: 0 0 auto;
    width: 1.125rem;
    height: 1.125rem;
    margin: 0;
    cursor: pointer;
    border-color: var(--aira-line-2);
}

.perm-card .master-show-check:checked {
    background-color: var(--perm-accent);
    border-color: var(--perm-accent);
}

.master-show-row .perm-row-label {
    min-width: 0;
    overflow-wrap: anywhere;
}

.permission-editor[data-readonly="true"] .master-show-row,
.permission-editor[data-readonly="true"] .master-global-right { cursor: default; }
.permission-editor[data-readonly="true"] .master-show-check:disabled { opacity: 1; }

/* Columns that each stack their own cards. A flowing grid sized every row to its tallest card, so
   the one-line Dashboard card stood beside a hole the height of Tickets. The server draws three
   lanes from PermissionGroupViewModel.Lane; settings.js (balancePermissionLanes) re-deals the cards
   into as many lanes as this grid has columns, levelled for the width it actually has.

   Every card is exactly as tall as its own content - never stretched to match a neighbour: a
   one-box Dashboard card drawn as tall as the eight-box Tickets card beside it was the defect this
   layout replaced. The columns come out close to level because of how the cards are dealt.

   The column count follows the editor's own width rather than the viewport: the same grid sits in
   a Settings card beside the sidebar and in a dialog that is nearly the whole screen. */
.perm-board {
    container: perm-board / inline-size;
}

.perm-modules {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
    align-items: start;
}

.perm-lane {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    min-width: 0;
}

.perm-card {
    --perm-accent: var(--ins-primary);
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--ins-secondary-bg);
    border: 1px solid var(--aira-line);
    border-top: 3px solid var(--perm-accent);
    border-radius: var(--ins-border-radius-lg);
    box-shadow: var(--aira-shadow-1);
    overflow: hidden;
}

/* Three columns only while each is wide enough to keep a hint on two lines. */
@container perm-board (max-width: 1180px) {
    .perm-modules { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@container perm-board (max-width: 700px) {
    .perm-modules { grid-template-columns: 1fr; }
}

.perm-card-head {
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    padding: .875rem 1rem;
    border-bottom: 1px solid var(--aira-line);
    background: linear-gradient(90deg, color-mix(in srgb, var(--perm-accent) 8%, transparent), transparent 75%);
}

.perm-card-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 8px;
    color: var(--perm-accent);
    background: color-mix(in srgb, var(--perm-accent) 15%, transparent);
}

.perm-card-icon svg { width: 18px; height: 18px; }

.perm-card-heading {
    flex: 1 1 auto;
    min-width: 0;
    align-self: center;
}

.perm-card-title {
    margin: 0;
    font-size: 14px;
    font-weight: 700;
    color: var(--aira-ink);
}

.perm-card-desc {
    margin: 3px 0 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--aira-ink-2);
}

.perm-count {
    flex: 0 0 auto;
    align-self: center;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    color: var(--perm-accent);
    background: color-mix(in srgb, var(--perm-accent) 13%, transparent);
}

/* Shown only while the card's prerequisite is off, and never on a read-only grid, where every
   box is disabled for a different reason. */
.perm-card-requires {
    display: none;
    align-items: center;
    gap: .375rem;
    padding: .5rem 1rem;
    font-size: 12px;
    color: var(--aira-ink-2);
    background: var(--aira-surface-2);
    border-bottom: 1px solid var(--aira-line);
}

.perm-card-requires svg { width: 13px; height: 13px; }

.permission-editor[data-readonly="false"] .perm-card:has(.perm-check:disabled:not([data-locked="true"])) .perm-card-requires {
    display: flex;
}

.perm-list {
    display: flex;
    flex-direction: column;
}

.perm-row {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin: 0;
    padding: .625rem 1rem;
    border-top: 1px solid var(--aira-line);
    cursor: pointer;
    transition: background-color .12s ease;
}

.perm-row:first-child { border-top: 0; }
.perm-row:hover { background: var(--aira-surface-2); }

/* A dependent row sits under its parent, with a short elbow saying so. */
.perm-row[data-depth="1"] { padding-left: 1.5rem; }
.perm-row[data-depth="2"] { padding-left: 2.75rem; }

.perm-row[data-depth="1"]::before,
.perm-row[data-depth="2"]::before {
    content: "";
    flex: 0 0 auto;
    align-self: flex-start;
    width: 9px;
    height: 10px;
    margin-top: 3px;
    border-left: 1.5px solid var(--aira-line-2);
    border-bottom: 1.5px solid var(--aira-line-2);
    border-bottom-left-radius: 3px;
}

.perm-row-text {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.perm-row-label {
    font-size: 13.5px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--aira-ink);
}

.perm-row-hint {
    font-size: 12px;
    line-height: 1.4;
    color: var(--aira-ink-2);
}

.perm-row-requires {
    display: none;
    font-size: 11.5px;
    color: var(--aira-muted);
}

.perm-check.form-check-input {
    flex: 0 0 auto;
    width: 1.125rem;
    height: 1.125rem;
    margin: 0;
    cursor: pointer;
    border-color: var(--aira-line-2);
}

.perm-card .perm-check:checked {
    background-color: var(--perm-accent);
    border-color: var(--perm-accent);
}

.perm-card .perm-check:focus-visible {
    border-color: var(--perm-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--perm-accent) 28%, transparent);
}

/* Granted. */
.perm-row:has(.perm-check:checked) {
    background: color-mix(in srgb, var(--perm-accent) 7%, transparent);
}

.perm-row:has(.perm-check:checked):hover {
    background: color-mix(in srgb, var(--perm-accent) 11%, transparent);
}

.perm-row:has(.perm-check:checked) .perm-row-label { font-weight: 600; }

/* Waiting on a parent that is off. */
.permission-editor[data-readonly="false"] .perm-row:has(.perm-check:disabled) { cursor: not-allowed; }
.permission-editor[data-readonly="false"] .perm-row:has(.perm-check:disabled):not(:has(.perm-check:checked)):hover { background: transparent; }
.permission-editor[data-readonly="false"] .perm-row:has(.perm-check:disabled:not([data-locked="true"])) .perm-row-label,
.permission-editor[data-readonly="false"] .perm-row:has(.perm-check:disabled:not([data-locked="true"])) .perm-row-hint { color: var(--aira-muted); }
.permission-editor[data-readonly="false"] .perm-row:has(.perm-check:disabled:not([data-locked="true"])) .perm-row-requires { display: block; }

/* Locked: not this editor's to change. The tick keeps full strength - what the person holds is
   still the point - and the reason sits on the row instead of the dependency note. */
.perm-check[data-locked="true"]:disabled { opacity: .75; }
.perm-row:has(.perm-check[data-locked="true"]) .perm-row-requires { display: none !important; }
.perm-matrix-hit:has(.perm-check[data-locked="true"]) { cursor: not-allowed; }

.perm-row-locked {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    font-size: 11.5px;
    color: var(--aira-muted);
}

.perm-row-locked svg { width: 11px; height: 11px; }

/* Read-only (the system administrator role): the theme fades a disabled box to 50%, which made
   granted and not granted look alike - the one thing this grid is for. */
.permission-editor[data-readonly="true"] .perm-check:disabled { opacity: 1; }
.permission-editor[data-readonly="true"] .perm-row,
.permission-editor[data-readonly="true"] .perm-matrix-hit { cursor: default; }
.permission-editor[data-readonly="true"] .perm-row:not(:has(.perm-check:checked)):hover { background: transparent; }

/* Stage tiles. */
.perm-list--tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
    gap: .625rem;
    padding: 1rem;
}

.perm-list--tiles .perm-row,
.perm-list--tiles .perm-row:first-child {
    align-items: flex-start;
    border: 1px solid var(--aira-line);
    border-radius: var(--ins-border-radius);
}

.perm-list--tiles .perm-row:has(.perm-check:checked) {
    border-color: color-mix(in srgb, var(--perm-accent) 45%, var(--aira-line));
}

.perm-list--tiles .perm-check { margin-top: 1px; }

.perm-swatch {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    margin-top: 5px;
    border-radius: 50%;
    background: var(--perm-swatch, var(--aira-muted));
}

/* The report matrix. */
.perm-matrix {
    --ins-table-bg: transparent;
}

.perm-matrix > :not(caption) > * > * {
    padding: .625rem 1rem;
    vertical-align: middle;
    border-bottom-color: var(--aira-line);
}

.perm-matrix thead th {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--aira-muted);
    background: var(--aira-surface-2);
}

.perm-matrix tbody th { font-weight: 400; }
.perm-matrix tbody tr:last-child > * { border-bottom: 0; }

.perm-matrix .perm-matrix-col,
.perm-matrix .perm-matrix-cell {
    width: 7.5rem;
    text-align: center;
}

.perm-matrix > tbody > tr > .perm-matrix-cell { padding: 0; }

.perm-matrix-hit {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 3rem;
    margin: 0;
    cursor: pointer;
}

/* The cells by tag, never `> *`: a :has() rule ending in a bare `*` made every chart label on the
   dashboard re-style the whole page (ubold-ui.md, "A :has() rule ends in a tag or a class"). */
.perm-matrix tbody tr:has(.perm-check:checked) > th,
.perm-matrix tbody tr:has(.perm-check:checked) > td {
    background: color-mix(in srgb, var(--perm-accent) 7%, transparent);
}

.perm-matrix tbody tr:has(.perm-check:checked) .perm-row-label { font-weight: 600; }

/* The boxes sit a screen-width away from the report's name; the hover carries the eye across. */
.perm-matrix tbody tr:hover > * { background: var(--aira-surface-2); }

.perm-matrix tbody tr:has(.perm-check:checked):hover > th,
.perm-matrix tbody tr:has(.perm-check:checked):hover > td {
    background: color-mix(in srgb, var(--perm-accent) 11%, transparent);
}

/* Accent text on a dark card. The brand blue that paints a checked box well is a dark smudge as
   the Dashboard card's count and icon, so the text is lifted towards white; fills stay the hue. */
html[data-bs-theme="dark"] .perm-count,
html[data-bs-theme="dark"] .perm-card-icon {
    color: color-mix(in srgb, var(--perm-accent) 62%, #fff);
}

/* Save stays in reach at the foot of a grid taller than the screen. Pulled out to the edges of
   the card or dialog it sits in, so it reads as that container's footer. */
.perm-actions {
    position: sticky;
    bottom: 0;
    z-index: 3;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: .5rem;
    margin-top: 1.25rem;
    padding: .875rem 0;
    background: var(--ins-secondary-bg);
    border-top: 1px solid var(--aira-line);
}

/* A sticky box sticks at its scroll container's PADDING edge, so `bottom: 0` inside .modal-body
   left the grid scrolling past under the bar for the depth of the body's padding. */
.modal-body .perm-actions {
    bottom: calc(-1 * var(--ins-modal-padding));
    margin-inline: calc(-1 * var(--ins-modal-padding));
    margin-bottom: calc(-1 * var(--ins-modal-padding));
    padding-inline: var(--ins-modal-padding);
}

.card-body .perm-actions {
    margin-inline: calc(-1 * var(--ins-card-spacer-x));
    margin-bottom: calc(-1 * var(--ins-card-spacer-y));
    padding-inline: var(--ins-card-spacer-x);
    border-radius: 0 0 var(--ins-card-inner-border-radius) var(--ins-card-inner-border-radius);
}

/* ==========================================================================
   Settings - User Management.

   The avatar falls back to initials that are rendered already and only un-hidden when the
   Employee software host fails to serve the photo - see Views/Users/_Avatar.cshtml. The
   fallback wrapper carries no display utility, so [hidden] is the only rule that applies to it.
   ========================================================================== */
.aira-avatar-initials {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    line-height: 1;
    letter-spacing: .02em;
    text-transform: uppercase;
}

.aira-avatar-photo { object-fit: cover; }

/* The theme ships no min-width utility, and a flex child holding a long name will not shrink
   under it without one - it pushes the actions column out of the card instead. */
.min-w-0 { min-width: 0 !important; }

/* Dimmed by the shared rule under *Loading*; a click on a row that is about to be replaced is lost. */
[data-user-rows].is-loading { pointer-events: none; }

/* One run open at a time in the sync history; the chevron says which. */
.history-chevron { transition: transform .15s ease; }
[aria-expanded="true"] > .history-chevron { transform: rotate(90deg); }

[data-history-toggle]:hover,
[data-history-toggle]:focus-visible { background: var(--ins-tertiary-bg); }

/* Six columns with a photo on each side: below this the picker and the reason chips collide. */
.mapping-table { min-width: 1040px; }
.mapping-employee-cell { min-width: 300px; }

.import-check-col { width: 2.5rem; }

/* ==========================================================================
   NOTIFICATIONS
   The topbar bell, the console, and the phone mock the message formats
   preview into.
   ========================================================================== */

/* ---- The bell -----------------------------------------------------------
   Modelled on the theme's own notification dropdown, and sharing the View as
   menu's width so the two sit the same way under the topbar. Nothing here
   takes a z-index: it is a Bootstrap .dropdown-menu and inherits the theme's,
   which keeps it clear of the 1070-1135 band the stacked-dialog arithmetic
   owns. */
.aira-bell-menu {
    min-width: 24rem;
    max-width: calc(100vw - 1.5rem);
    overflow: hidden;
}

.aira-bell-scroll {
    max-height: 26rem;
    min-height: 6rem;
}

.aira-bell-item {
    display: flex;
    gap: .75rem;
    align-items: flex-start;
    width: 100%;
    padding: .625rem .875rem;
    border: 0;
    border-bottom: 1px solid var(--aira-line);
    background: transparent;
    text-align: left;
    position: relative;
}

.aira-bell-item:last-child { border-bottom: 0; }

.aira-bell-item:hover,
.aira-bell-item:focus-visible { background: var(--ins-tertiary-bg); }

/* Unread is a ground and a weight, not a colour - a tinted row reads as a
   status ("something is wrong with this one") rather than as "you have not
   looked at this yet". */
.aira-bell-item.is-unread { background: var(--aira-surface-2); }
.aira-bell-item.is-unread .aira-bell-title { font-weight: 650; }

.aira-bell-icon {
    flex: 0 0 2rem;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    display: grid;
    place-items: center;
}

.aira-bell-icon svg { width: 1rem; height: 1rem; }

/* min-width:0 is load-bearing on a flex child: without it a long customer name
   pushes the row wider than the menu instead of wrapping inside it. */
.aira-bell-body { flex: 1 1 auto; min-width: 0; }

.aira-bell-title {
    display: block;
    font-size: .8125rem;
    color: var(--aira-ink);
    line-height: 1.35;
}

/* The body is operator-authored template text whose line breaks are part of
   the message - the same breaks that reach the phone. Four lines in the
   dropdown: a ten-line daily report must not push everything else below the
   fold. The whole thing is shown on the notifications page. */
.aira-bell-text {
    margin-top: .125rem;
    font-size: .75rem;
    color: var(--aira-ink-2);
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.aira-bell-meta {
    display: block;
    margin-top: .1875rem;
    font-size: .6875rem;
    color: var(--aira-muted);
}

.aira-bell-redacted { font-style: italic; color: var(--aira-muted); }

.aira-bell-dot {
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    margin-top: .5rem;
    border-radius: 50%;
    background: var(--ins-primary);
}

.aira-bell-state {
    padding: 2rem 1.25rem;
    text-align: center;
    font-size: .8125rem;
    color: var(--aira-ink-2);
}

.aira-bell-loading {
    padding: .75rem;
    text-align: center;
}

.aira-bell-foot .dropdown-item:hover { background: var(--ins-tertiary-bg); }

/* ---- The phone mock -----------------------------------------------------
   From the approved prototype, and deliberately dark in both colour modes:
   it is imitating a phone, not a panel of this application. The hex values
   are the prototype's own and are correct as constants for that reason. */
.pulse-phone {
    background: #0d1728;
    border-radius: 18px;
    padding: 14px;
    box-shadow: var(--aira-shadow-2);
}

.pp-top {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 2px 4px 12px;
}

.pp-logo {
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    border-radius: 9px;
    display: grid;
    place-items: center;
    background: linear-gradient(150deg, #2a78d6, #1b56a0);
    color: #fff;
    font-weight: 800;
    font-size: 12.5px;
}

.pp-app { color: #fff; font-size: 13px; font-weight: 650; line-height: 1.25; }
.pp-sub { color: #7f8fa8; font-size: 11px; }
.pp-time { margin-left: auto; color: #7f8fa8; font-size: 11px; }

.pp-bubble {
    background: #16233c;
    border: 1px solid #26334d;
    border-radius: 12px;
    padding: 14px 15px;
}

/* pre-wrap, because the line breaks in a message format ARE the message. */
.pp-bubble pre {
    margin: 0;
    color: #dbe4f0;
    font-family: inherit;
    font-size: 12.5px;
    line-height: 1.75;
    white-space: pre-wrap;
    word-break: break-word;
}

.pp-attach {
    display: flex;
    align-items: center;
    gap: 11px;
    margin-top: 13px;
    padding-top: 13px;
    border-top: 1px solid #26334d;
}

.pp-file {
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    border-radius: 8px;
    background: #d03b3b;
    color: #fff;
    display: grid;
    place-items: center;
    font-size: 10px;
    font-weight: 800;
}

.pp-attach .nm { color: #dbe4f0; font-size: 12px; font-weight: 600; word-break: break-all; line-height: 1.35; }
.pp-attach .mt { color: #7f8fa8; font-size: 11px; margin-top: 2px; }

/* ---- The console --------------------------------------------------------- */

/* The three switches that all have to be on. A dot and two lines, because the
   answer an operator needs is "on or off" and then "what is lost". */
.notif-health {
    display: flex;
    gap: .625rem;
    align-items: center;
    padding: .625rem .75rem;
    border: 1px solid var(--aira-line);
    border-radius: .5rem;
    background: var(--aira-surface-2);
}

.notif-health-dot {
    flex: 0 0 .625rem;
    width: .625rem;
    height: .625rem;
    border-radius: 50%;
    background: var(--ins-secondary);
}

.notif-health.is-success .notif-health-dot { background: var(--ins-success); }
.notif-health.is-warning .notif-health-dot { background: var(--ins-warning); }
.notif-health.is-danger .notif-health-dot { background: var(--ins-danger); }
.notif-health.is-danger { border-color: color-mix(in srgb, var(--ins-danger) 35%, transparent); }

.notif-health-body { min-width: 0; }
.notif-health-label { display: block; font-size: .75rem; font-weight: 650; color: var(--aira-ink); }
.notif-health-state { display: block; font-size: .6875rem; color: var(--aira-ink-2); }

.notif-stat {
    padding: .5rem .75rem;
    border-left: 3px solid var(--aira-line-2);
    background: var(--aira-surface-2);
    border-radius: .25rem;
}

.notif-stat .l { font-size: .6875rem; color: var(--aira-muted); line-height: 1.3; }

/* Tabular figures so a column of counts lines up rather than dancing. */
.notif-stat .v {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--aira-ink);
    font-variant-numeric: tabular-nums;
}

.notif-stat .v .of { font-size: .875rem; font-weight: 500; color: var(--aira-muted); }
.notif-stat.is-busy { border-left-color: var(--ins-warning); }
.notif-stat.is-bad { border-left-color: var(--ins-danger); }

.notif-passes td { font-size: .75rem; color: var(--aira-ink-2); }
.notif-definitions td,
.notif-definitions th { font-size: .75rem; }

/* The token palette. Monospace, because they are typed into a message body. */
.aira-token {
    font-family: var(--ins-font-monospace, monospace);
    font-size: .6875rem;
    padding: .1875rem .375rem;
    margin: 0 .25rem .25rem 0;
    border: 1px dashed var(--aira-line-2);
}

.aira-token:hover { border-style: solid; border-color: var(--ins-primary); }

/* A rule that is on but reaches nobody is the commonest misconfiguration here,
   and it is completely silent from every other screen. */
.notif-rule-empty { color: var(--ins-danger); }

.notif-rule-dot {
    display: inline-block;
    width: .5rem;
    height: .5rem;
    border-radius: .125rem;
    margin-right: .5rem;
    vertical-align: middle;
}

/* The message a format renders, in a list. One line, however long it is. */
.notif-preview-cell {
    max-width: 22rem;
    font-size: .75rem;
    color: var(--aira-ink-2);
}

/* The personal notifications page: the same row, unclamped, because this is
   where somebody goes to read the whole thing. */
.aira-inbox-list .aira-bell-item { padding: .875rem 1rem; }
.aira-inbox-list .aira-bell-text { -webkit-line-clamp: unset; display: block; }

/* ==========================================================================
   AUDIT LOG
   One feed of sentences (C14, audit.md): a day heading, then a row per thing
   somebody did - the time, the category's icon, the sentence, and a chevron
   where there is a before and after to read.
   ========================================================================== */

.audit-person-pick { min-width: 16rem; }

.audit-day {
    padding: .5rem 1rem;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--aira-ink-2);
    background: var(--ins-tertiary-bg);
    border-bottom: 1px solid var(--aira-line);
}

.audit-row { border-bottom: 1px solid var(--aira-line); }
.audit-row:last-child { border-bottom: 0; }
.audit-row:has([data-audit-expand]) { cursor: pointer; }
.audit-row.is-open { background: var(--ins-tertiary-bg); }

.audit-row-main {
    display: grid;
    grid-template-columns: 5.25rem 1.75rem minmax(0, 1fr) auto;
    align-items: start;
    gap: .5rem;
    padding: .625rem 1rem;
}

.audit-time {
    font-size: .8125rem;
    color: var(--aira-ink-2);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    padding-top: .0625rem;
}

.audit-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    font-size: .875rem;
    color: var(--ins-primary);
    background: var(--ins-primary-bg-subtle);
}

.audit-sentence { margin: 0; padding-top: .125rem; overflow-wrap: anywhere; }
.audit-person-link, .audit-ticket { font-weight: 600; }

.audit-expand { padding: .125rem .375rem; line-height: 1; }
.audit-expand svg { transition: transform .15s ease; }
.audit-expand[aria-expanded="true"] svg { transform: rotate(90deg); }

/* Under the sentence, lined up with it rather than with the time. */
.audit-details { padding: 0 1rem .75rem calc(1rem + 5.25rem + 1.75rem + 1rem); cursor: auto; }
.audit-note { white-space: pre-line; color: var(--aira-ink-2); }
.audit-changes { max-width: 48rem; background: transparent; }
.audit-changes > :not(caption) > * > * { background: transparent; }
.audit-changes thead th { font-size: .75rem; font-weight: 600; color: var(--aira-ink-2); }
.audit-changes .audit-label { font-weight: 600; width: 30%; }
.audit-changes .audit-before { color: var(--aira-ink-2); text-decoration: line-through; text-decoration-color: var(--aira-line-2); }

/* The Person view's five figures, each a toggle that narrows the feed to exactly its count. */
.audit-person-head { margin-bottom: .75rem; }
.audit-figures { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .5rem; }
.audit-figure {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: .125rem;
    text-align: left;
    border: 1px solid var(--aira-line-2);
}
.audit-figure-count { font-size: 1.25rem; font-weight: 700; line-height: 1.2; }
.audit-figure-label { font-size: .75rem; }

@media (max-width: 767.98px) {
    /* The phone reads a row as its time over its sentence. */
    .audit-row-main { grid-template-columns: 1.75rem minmax(0, 1fr) auto; grid-template-areas: "icon time more" "icon text more"; column-gap: .625rem; row-gap: .125rem; }
    .audit-time { grid-area: time; }
    .audit-icon { grid-area: icon; }
    .audit-sentence { grid-area: text; }
    .audit-expand { grid-area: more; }
    .audit-details { padding-left: calc(1rem + 1.75rem + .625rem); }

    /* Label, then before over after - a three-column table does not fit a phone. */
    .audit-changes thead { display: none; }
    .audit-changes tr { display: grid; grid-template-columns: 1fr; padding: .25rem 0; }
    .audit-changes tr > *, .audit-changes .audit-label { border: 0; padding: .0625rem 0; width: auto; }
    .audit-changes .audit-before::before { content: "Before: "; color: var(--aira-ink-2); text-decoration: none; display: inline-block; margin-right: .25rem; }
    .audit-changes .audit-after::before { content: "After: "; color: var(--aira-ink-2); margin-right: .25rem; }

    .audit-figures { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .audit-person-pick { min-width: 0; flex: 1 1 100%; }
}

/* --------------------------------------------------------------------------
   App full screen (app/js/aira-fullscreen.js; ubold-ui.md, *App full screen*).
   The tab's document hosts the app in this frame and puts the frame in native
   full screen, so a navigation - which happens inside it - never leaves full
   screen. Out of full screen the frame stays over the whole window until the
   next navigation, so work in progress survives. No backdrop-filter (hard rule
   10b). html.fullscreen-enable has one job left: the theme's topbar icons.
   -------------------------------------------------------------------------- */
.aira-fs-frame {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    background: var(--ins-body-bg);
    z-index: var(--aira-z-app-frame);
}

/* The host page behind the frame gets no scrollbar of its own - nor the Tickets page's reserved
   gutter, which a fixed frame does not cover: out of full screen it was an empty strip beside it. */
html[data-aira-fs-host] { overflow: hidden; scrollbar-gutter: auto; }
