/* Bootstrap does not provide these. Panel rules arrive in Task 7. */

/* ── shell ── */
.app-shell { display: flex; min-height: 100vh; background: var(--surface); }

.app-rail {
    width: 56px; flex: none;
    background: var(--surface-raised);
    border-right: 1px solid var(--border);
    display: flex; flex-direction: column; align-items: center;
    gap: var(--space-2); padding: var(--space-3) 0;
}
/* The brand was a bare inline <a> around a 30px-wide logo. The logo's own aspect
   ratio (1234 x 333) made that 30 x 8.1px — an 8px-tall click target for the only
   "go home" link in the app. It is a link, so it is sized like one you can hit:
   36 x 36, matching .rail-item's own target.
   The image inside it is cropped to the cube mark rather than scaled down whole.
   At 30px wide the wordmark beside the cube was 3px tall — unreadable in either
   theme, and in dark mode invisible as well (its baked-in #042c75 reads 1.31:1 on
   --surface-raised). object-fit: cover on a square box scales the SVG by its
   height and shows the leftmost square of the viewBox, which is exactly the cube:
   full size, full brand colour, and independent of the theme behind it. */
.rail-brand {
    width: 36px; height: 36px; flex: none;
    margin-bottom: var(--space-3);
    border-radius: var(--radius-sm);
    display: inline-flex; align-items: center; justify-content: center;
}
.rail-brand:hover { background: var(--surface-sunken); }
/* 24 x 30 is the cube's own aspect ratio inside the file (268.45 x 333.05 of a
   1234 x 333.05 viewBox), so `cover` scales the artwork by its height and the
   leftmost 24px it keeps is the mark, ending where the wordmark begins. */
.rail-brand img {
    width: 24px; height: 30px; display: block;
    object-fit: cover; object-position: left center;
}
.rail-foot { margin-top: auto; display: flex; flex-direction: column; gap: var(--space-2); }

.rail-item {
    width: 38px; height: 38px; flex: none; border: 0; background: transparent;
    border-radius: var(--radius-sm);
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--text-muted); text-decoration: none;
    transition: background .15s, color .15s;
}
.rail-item:hover { background: var(--surface-sunken); color: var(--text); }
.rail-item.is-active { background: var(--accent-subtle); color: var(--accent); }

/* A1 — the composer's own collapse control lives INSIDE the composer, so
   collapsing it hid the only button that reopens it; the state persists to
   localStorage, so a reload did not recover either, and "[" is undiscoverable
   and useless on touch. The rail is the one column on screen in every state, so
   the reopen affordance lives here: a real <button> in tab order, with an
   accessible name and an aria-expanded that _Layout keeps in step with
   body[data-rail]. This is the visual half of that state — deliberately the
   sunken ground rather than .rail-item.is-active's accent-subtle, so a pressed
   toggle never reads as "this is the section you are in" two icons below the
   section that actually is. --accent on --surface-sunken measures 5.02:1 light,
   7.05:1 dark. */
body[data-rail="open"] .rail-toggle,
body[data-rail="open"] .rail-toggle:hover { background: var(--surface-sunken); color: var(--accent); }

/* ── composer ── */
/* --surface-raised, not transparent. On desktop the composer used to borrow
   .app-shell's --surface by having no background of its own; below 992px the
   media query at the bottom of this block promotes it to position:fixed, and a
   transparent floating drawer let the results column read straight through it.
   Raised is also the right value on its own terms: it matches .app-rail, so the
   shell chrome reads as one surface against the tinted results ground, and it
   puts the composer's controls on the background --accent and --text-muted are
   contrast-verified against (TokenContrastTests) — --accent on --surface is
   4.36:1, below the AA floor for the small button text this column carries. */
.app-composer {
    /* 300px was tight before the composer carried a suggestion list and a library
       button: a prompt long enough to name a metric wrapped across three monospace
       lines, and every suggestion row wrapped onto two. Wider is not decoration
       here — it is the difference between reading a suggestion and guessing at it.
       Capped against the viewport so a narrow laptop is not left with a rail and
       no dashboard. */
    /* The clamp is the DEFAULT, not the rule: --composer-w is what a drag writes, and
       it wins whenever it is set. Keeping the fallback in the var() means a reader who
       has never dragged (or who resets) gets exactly the old behaviour. */
    width: var(--composer-w, clamp(340px, 32vw, 520px)); flex: none;
    background: var(--surface-raised);
    padding: var(--space-4);
    border-right: 1px solid var(--border);
    display: flex; flex-direction: column; gap: var(--space-3);
    overflow-y: auto;
}
body[data-rail="collapsed"] .app-composer { display: none; }

/* A5 — theme.js only learns the stored collapse state on DOMContentLoaded, so a
   user who had collapsed the composer still got a 300px column painted first and
   watched it vanish on every single load. The theme already solves this with a
   pre-paint snippet in <head>; the rail state now gets the same treatment, and
   this is the selector that snippet drives. It is deliberately scoped to
   `body:not([data-rail])` — the window between first paint and theme.js writing
   the real attribute. The moment theme.js runs, the rule above takes over and
   this one stops matching, so the two can never disagree about the current
   state (which is exactly what a second, independent copy of it would risk). */
html.rail-collapsed-preload body:not([data-rail]) .app-composer { display: none; }

/* Collapse control sits at the top of the composer column. */
.composer-collapse {
    align-self: flex-end;
    width: 30px; height: 30px; border: 0;
    background: var(--surface-sunken);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    display: inline-flex; align-items: center; justify-content: center;
    transition: background .15s, color .15s;
}
.composer-collapse:hover { background: var(--border); color: var(--text); }

/* A finger-sized close control on the overlay drawer: the 30px desktop button is under the
   ~44px touch guideline, and on a phone this is the reader's way to dismiss the drawer that
   covers the dashboard (audit R-06). */
@media (max-width: 991.98px) {
    .composer-collapse { width: 44px; height: 44px; font-size: 1.15rem; }
}

/* .app-shell uses min-height (a fixed height would leave the Users screens'
   bare flex child overflowing), so this is what makes the rail and composer's
   own overflow-y actually bite: sticky pins each to the viewport instead of
   scrolling away with the document, and align-self stops the flex line
   stretching them to the height of a long results column.

   `height`, not `max-height`. max-height only caps a box — it cannot give one a
   size — so with align-self: flex-start both columns fell back to content
   height: the rail measured 232px against a 742px viewport (its --surface-raised
   background stopped a third of the way down and .rail-foot's margin-top: auto
   parked the theme and account buttons in mid-air), and the composer stopped
   wherever its own controls ended, leaving the "Saved Reports" button's mt-auto
   inert. An explicit height makes both columns the viewport, which is what
   sticky, overflow-y and both margin-top: autos were written against.

   It also fixes the drawer below 992px (A4). There .app-composer is
   position: fixed with inset: 0 auto 0 56px, which looks like it should fill
   top to bottom — but CSS Box Alignment sizes an absolutely positioned box as
   fit-content, not stretch, whenever align-self is anything other than
   normal/stretch, even with both offsets definite. So the drawer was ALSO
   content height (measured: 681.8px in a 741.6px viewport) and dashboard panels
   showed through the gap underneath it. A definite height leaves nothing for
   that rule to shrink. */
.app-rail,
.app-composer {
    position: sticky; top: 0; align-self: flex-start;
    height: 100vh;
    height: 100dvh;   /* mobile URL bar; ignored where dvh is unsupported */
}

/* The rail has to out-rank the composer, and `position: sticky` is why it did
   not. Sticky establishes a stacking context unconditionally — z-index: auto
   does not opt out of that the way it does for a plain positioned box — so the
   account dropdown's own z-index: 1000 is resolved INSIDE the rail's context and
   can never lift the menu past a sibling of the rail. Both columns then painted
   in DOM order, and the composer comes second, so it covered every part of a
   160px-wide menu past the rail's own 56px: `document.elementFromPoint` at the
   centre of "Logout" returned the composer, and clicking there activated
   whichever composer control was underneath rather than logging out.

   Ordering the two contexts against each other is the whole fix — the menu then
   rides along with the rail — and it changes nothing about the sticky
   positioning that keeps both columns pinned while the results column scrolls. */
.app-composer { z-index: 1; }
.app-rail     { z-index: 2; }

/* ── results ── */
.app-results {
    flex: 1; min-width: 0;
    padding: var(--space-4) var(--space-5);
    overflow-y: auto;
    container-type: inline-size;   /* Task 10's grid measures against this */
    container-name: results;
}

@media (max-width: 991.98px) {
    /* The drawer is app chrome floating over the results column, so it belongs in
       Bootstrap's "fixed" band ($zindex-fixed: 1030), not on 1040 — which is
       $zindex-offcanvas-backdrop, the dim behind the Saved Reports drawer, and
       being level with it only ever meant "whichever came later in the DOM wins".
       Nothing in this app sits between 1030 and 1040, so the drawer still covers
       everything it covered before; what changes is that the offcanvas backdrop is
       now unambiguously above it, and the rail below can be lifted over the drawer
       (same reason as the desktop pair above — the account menu overhangs the
       rail's 56px and would otherwise be swallowed by an open drawer) while both
       columns stay under that backdrop and get dimmed with the rest of the page. */
    .app-composer {
        position: fixed; inset: 0 auto 0 56px; z-index: 1030;
        width: min(320px, calc(100vw - 56px));
        box-shadow: var(--shadow-lg);
        transform: translateX(-100%); transition: transform .2s;
    }
    .app-rail { z-index: 1031; }
    body[data-rail="open"] .app-composer { transform: none; display: flex; }
}

/* ── dashboard grid (Task 10) ────────────────────────────────────────────────
   Spec §7's table has four columns — Wide / Laptop-rail-open / Tablet / Phone —
   and they are NOT four points on one axis of container width. The composer
   leaves the flow at 992px (see the drawer rule above), so — measuring what a
   container query actually sees, which is .app-results' CONTENT box, i.e. the
   column minus its 48px of horizontal padding (verified: the bracket below
   flips between 1196 and 1211, not between 1244 and 1259):

       viewport   rail        query size   §7 column
       1920       open           1516      Wide
       1920       collapsed      1816      Wide
       1440       open           1036      Laptop, rail open
       1440       collapsed      1321      Wide
       1024       open            605      Laptop, rail open
       1024       collapsed       905      Laptop, rail open
        768       either          649      Tablet
        375       either          271      Phone

   Tablet's results column (649) is WIDER than laptop-rail-open's at 1024 (605)
   while the table asks it to be coarser ([quarter] 6/12 vs 4/12, [twothirds]
   full vs 8/12). No single container-width threshold can order those two,
   which is why the previous 700px cutoff swallowed 1024-rail-open whole and
   collapsed every width hint to full width. The two ranges are separated by
   the shell breakpoint instead of by arithmetic:

   - Below 992px the composer is a fixed overlay, so the results column is
     always the window minus the 56px rail and the rail state cannot re-flow
     anything. A container query there would only restate the viewport, so
     Phone and Tablet are plain media queries.
   - At and above 992px the composer is in flow, so collapsing it genuinely
     changes the results column. Only here does a container query carry
     information the viewport does not, and only here is one used.

   The single desktop threshold is 1200px: it sits in the gap between the widest
   laptop-rail-open case (1036) and the narrowest Wide case (1321), far enough
   from both that a scrollbar appearing or a zoom step cannot flip a bracket,
   and it is also exactly the width at which a [quarter] panel at 3/12 still
   gets the 300px the KPI tile was drawn at. ── */

/* Phone. Bootstrap's own col-md-* stops applying below 768px and .row > *
   forces 100%; state it anyway so a col-12 or a stray col-* cannot drift. */
@media (max-width: 767.98px) {
    .dashboard-grid > [class*="col-"] { flex: 0 0 100%; max-width: 100%; }
}

/* Tablet: [twothirds] drops to a full row, [third] and [quarter] pair up. */
@media (min-width: 768px) and (max-width: 991.98px) {
    .dashboard-grid > .col-md-8 { flex: 0 0 100%; max-width: 100%; }
    .dashboard-grid > .col-md-4,
    .dashboard-grid > .col-md-3 { flex: 0 0 50%; max-width: 50%; }
}

/* Desktop. Above the threshold nothing is overridden and Bootstrap's own
   col-md-* is already the Wide column (8/12, 6/12, 4/12, 3/12). Below it,
   [third] widens to 6/12 and [quarter] to 4/12 per the Laptop column;
   [twothirds] and [half] read the same in both, so they need no rule. */
@media (min-width: 992px) {
    @container results (max-width: 1200px) {
        .dashboard-grid > .col-md-4 { flex: 0 0 50%; max-width: 50%; }
        .dashboard-grid > .col-md-3 { flex: 0 0 33.3333%; max-width: 33.3333%; }
    }
}

/* The Organize toggle sits first in the export group but is not an export: a little more room
   after it. On, Bootstrap's .active fills it (aria-pressed says the same to a screen reader). */
#dashExportBtns > .organize-toggle { margin-inline-end: .5rem; }

/* ── "Organize charts" (the #organizeBtn beside the exports, js/dashboard/organize.js) ──
   Off, nothing here applies and the prompts' own col-* widths and order stand. On, organize.js
   writes each panel's place and share of its row as --org-order and --org-basis, and these rules
   are all that read them — beating every col-* width above, the container-query ones included.
   The basis is a share less 2px and the panel grows back into it, so a row of exact thirds cannot
   round over and drop its last panel onto a line of its own. min-width: 0 so a panel's content
   cannot widen it past its share and break the planned rows. Reordered with `order`, not DOM
   moves, so streaming, Refresh, corrections and PDF export need know nothing about it. Keyboard
   focus still follows prompt order — the trade for an opt-in view.

   A panel the plan has not reached yet (the frame between a streamed panel landing and its
   re-plan) sorts last rather than first, and a streaming skeleton keeps its width at the end. */
.dashboard-grid.is-organized > .dashboard-panel {
    order: var(--org-order, 99999);
    flex: 1 1 var(--org-basis, calc(50% - 2px));
    max-width: 100%;
    min-width: 0;
}
.dashboard-grid.is-organized > [data-panel-placeholder] { order: 100000; }

/* Between Index.cshtml's early script and organize.js's first plan, when the stored choice is on.
   The animation is only the fallback that shows the panels anyway if organize.js never runs. */
.dashboard-grid.organize-pending > .dashboard-panel {
    visibility: hidden;
    animation: organize-reveal 0s 1.5s forwards;
}
@keyframes organize-reveal { to { visibility: visible; } }

/* ── The dashboard grid (spec 013, js/features/dashboards/layout-grid.js) ──
   Gridstack positions each .grid-stack-item; the panel inside fills it. Gridstack's margin is an
   inset on every side of .grid-stack-item-content (8px), so cards are 16px apart; the wrapper's 8px
   padding makes the outer edge 16px too, as ECFront's gridster draws it, and its -16px margin puts
   the cards back on the edges the Bootstrap row put them on. Panels keep their col-* class from the
   template and Refresh; inside a grid item it means nothing, so it is neutralised here. */
.layout-grid-pad { margin: -16px; padding: 8px; }
.grid-stack.layout-pending > .grid-stack-item {
    visibility: hidden;
    animation: organize-reveal 0s 1.5s forwards;
}
.grid-stack > .grid-stack-item > .grid-stack-item-content { overflow: visible; }
.grid-stack-item-content > .dashboard-panel,
.grid-stack-item-content > [data-panel-placeholder] {
    width: 100%; max-width: 100%; height: 100%;
    margin: 0 !important; padding: 0; flex: none;
}
.grid-stack-item-content > .dashboard-panel > .panel,
.grid-stack-item-content > [data-panel-placeholder] > .panel { height: 100%; min-height: 0; }
/* ── A card as small as the grid lets it be (the user, 15 Oct: "thumbs up/down disappear if chart size
   decreased"). Measured on the real cards at their kinds' minimums: the foot and the thumbs-down box are
   the parts that cannot shrink, and .panel clips, so what did not fit was cut off the bottom - the
   thumbs of a small figure card, and up to 422px of the thumbs-down box, whose opening also squeezed the
   chart out of its card entirely. The head and the foot keep their size; only the body gives. */
.grid-stack-item-content .panel { position: relative; }
.grid-stack-item-content .panel > .panel-head,
.grid-stack-item-content .panel > .panel-foot { flex: none; }
/* The thumbs-down box (and the line that answers it) opens OVER the dashboard, under the card's foot,
   as a menu does: in the card it took the chart's room and was cut off anyway. At least 300px wide, so a
   quarter-width card still shows its reasons readably; panel-actions.js turns it to open from the
   card's start edge when its end edge would put it past the dashboard. */
.grid-stack-item-content .panel > .feedback-detail,
.grid-stack-item-content .panel > .feedback-said {
    position: absolute; top: calc(100% + 4px); inset-inline-end: 0; z-index: 5;
    width: max(100%, 300px); max-width: calc(100vw - 2rem);
    margin: 0 !important; padding: var(--space-3) !important;
    background: var(--surface-raised); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg, var(--shadow-md));
}
.grid-stack-item-content .panel > .feedback-detail.opens-from-start,
.grid-stack-item-content .panel > .feedback-said.opens-from-start { inset-inline-end: auto; inset-inline-start: 0; }
.grid-stack-item-content .panel > .feedback-detail.opens-upward,
.grid-stack-item-content .panel > .feedback-said.opens-upward { top: auto; bottom: calc(100% + 4px); }
.grid-stack-item-content .panel:has(> .feedback-detail:not(.d-none)),
.grid-stack-item-content .panel:has(> .feedback-said:not(.d-none)) { overflow: visible; }
.grid-stack > .grid-stack-item:has(.feedback-detail:not(.d-none)),
.grid-stack > .grid-stack-item:has(.feedback-said:not(.d-none)) { z-index: 30; }
/* Short and narrow cards: the card is a size container (container-type below, with the tile marker's
   rule), so its own parts tighten to fit it rather than the window. */
@container (max-height: 220px) {
    .panel > .panel-head { padding-top: var(--space-2); }
    .panel > .panel-body { padding-block: var(--space-1); }
    .panel > .panel-foot { padding-block: var(--space-1); }
    .panel-feedback .panel-action { width: 28px; height: 28px; }
    .panel-foot .panel-badge, .panel-foot .panel-timing { display: none; }
}
@container (max-width: 260px) {
    .panel > .panel-head { gap: var(--space-1); padding-inline: var(--space-2); }
    .panel-head .panel-action { width: 22px; }
    .panel-title { font-size: var(--text-sm); padding-inline: 2px; }
    .panel > .panel-foot { padding-inline: var(--space-2); }
}
/* Two columns of a narrow results area (83px at a 1024px window): the grip, Re-run and ⋮ still fit,
   the title keeps what is left (measured 15 Oct: ⋮ ran 7px past the card). */
@container (max-width: 150px) {
    .panel > .panel-head { gap: 2px; padding-inline: var(--space-1); }
    .panel-head .panel-grip { padding: 0; margin: 0; }
    .panel-head .panel-action { width: 18px; }
    .panel-title { font-size: var(--text-xs); padding-inline: 0; }
}
/* The body takes what the header leaves; a table or a long list scrolls inside its cell. */
.grid-stack-item-content .panel-body { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
/* A figure card has nothing to scroll to: the 9px it runs over in a 3-row cell is the top margins of
   the empty drill / SQL rows under the number (measured 14 Oct), not content. */
.grid-stack-item-content .panel-body:has(> .kpi) { overflow: hidden; }
/* Nor does a chart: ApexCharts draws a doughnut's legend a few px past the height it is given, and a
   scrollbar that overflow summoned would take 15px off the chart's width and send it round its
   redraw again (measured 14 Oct: 708 → 693px). Its drill-down table, under it, is short. */
.grid-stack-item-content .panel-body:has(> .chart-wrapper) { overflow: hidden; }
.grid-stack-item-content .chart-wrapper { overflow: hidden; }
/* A chart fills its cell (FR-002). The template's fixed 280px / 320px stand outside the grid, and so
   does the min-height fitHost (apex-builders.js) writes on every build: inline, it beat a plain 120px
   here, so a chart made shorter than 280px was redrawn at 280 and its card cut the bottom off. */
.grid-stack-item-content .chart-wrapper { flex: 1 1 auto; min-height: 120px !important; height: auto !important; margin-bottom: 0 !important; }
.grid-stack-item-content .chart-wrapper > [id^="chart_"] { height: 100% !important; min-height: 0 !important; }

/* The drag handle: the title is an input and fills the header, so a grip leads it. */
.panel-grip {
    flex: none; align-self: stretch;
    display: inline-flex; align-items: center;
    padding: 0 .25rem; margin-inline-start: -.25rem;
    color: var(--text-muted); cursor: grab; touch-action: none;
}
.panel-grip:hover { color: var(--text); }
.grid-stack-static .panel-grip,
.grid-stack-static .panel-menu-arrange { display: none; }
.ui-draggable-dragging .panel-grip { cursor: grabbing; }
.grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content > .dashboard-panel > .panel,
.grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content > .dashboard-panel > .panel {
    box-shadow: var(--shadow-lg, var(--shadow-md));
    outline: 2px solid var(--accent);
}
/* Where a dragged panel will land. */
.grid-stack > .grid-stack-placeholder > .placeholder-content {
    background: var(--accent-subtle);
    border: 2px dashed var(--accent);
    border-radius: var(--radius-md);
}

/* ── panel header (Task 7) ── */
/* height: auto with min-height: 100%, not height: 100%, so that a card can be
   TALLER than its grid row when something is opened inside it. The row still
   levels the cards: min-height resolves against the stretched column, so a panel
   that grows raises the line and every sibling stretches up with it (measured
   4-up: one panel forced to 1534.5px took its row-mates to 1534.5px too).

   With height: 100% the card could not grow at all, and the drawers opened from
   the menu — Show SQL, Performance, the raw-row table — simply overflowed .panel-body
   and painted over .panel-foot, which carries the feedback thumbs. Measured on
   "Risks Above Appetite" with Show SQL open: the body overflowed by 443px and the
   SQL block ended 442.6px below the TOP of the foot, straight across the thumbs.
   The same applies to .feedback-detail, which opens below the foot. */
.panel {
    background: var(--surface-raised);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    height: auto; min-height: 100%;
    display: flex; flex-direction: column;
    overflow: hidden;
}
/* The ⋮ menu is a Bootstrap dropdown nested inside .panel, so the clip above
   cuts it off on a short panel. Lift the clip only while a menu is open. */
.panel:has(.dropdown-menu.show) { overflow: visible; }
.panel-head {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-3) var(--space-3) 0;
}
/* No .panel-type-icon rule any more, and no element for it. The header carries only
   .panel-status — see the panel-head comment in Views/Shared/_DashboardPanel.cshtml. A
   permanent glyph in this slot competes with the one mark that has to be noticed, and for
   a while it WAS that mark: the KPI glyph chosen for "risk" was fa-triangle-exclamation,
   which is what panel-status--warn uses. */

.panel-title {
    flex: 1; min-width: 0;
    border: 1px solid transparent; background: transparent;
    border-radius: var(--radius-sm);
    padding: 2px 6px;
    font-size: var(--text-md); font-weight: 650; letter-spacing: -.01em;
    color: var(--text);
    text-overflow: ellipsis;
}
.panel-title:hover  { background: var(--surface-sunken); }
/* No outline:none here. The border-colour swap alone measured 1.32:1 (light) /
   2.06:1 (dark) against its own focus background — nowhere near the 3:1 WCAG
   wants — and this input is the only editable control in a panel and the target
   of ⋮ → Rename, so it keeps base.css's global :focus-visible ring like every
   other control. Text inputs match :focus-visible on click as well as on Tab,
   so the ring shows however the field is reached. */
.panel-title:focus  { background: var(--surface-sunken); border-color: var(--border-strong); }

.panel-action {
    width: 26px; height: 26px; flex: none;
    border: 0; background: transparent; border-radius: var(--radius-sm);
    color: var(--text-subtle); font-size: var(--text-xs);
    display: inline-flex; align-items: center; justify-content: center;
}
.panel-action:hover { background: var(--surface-sunken); color: var(--text); }

/* The Display group in the panel ⋮ menu (_PanelMenu.cshtml). Form controls
   rather than dropdown-items, so they carry the item padding themselves and hold a label and a
   control on one row. */
.panel-menu .dropdown-menu { min-width: 15rem; }
.panel-menu-control { padding: var(--space-1) var(--space-3); }
.panel-menu-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    font-size: var(--text-sm); color: var(--text); white-space: nowrap; margin: 0;
}
.panel-menu-row.form-switch { padding-left: 0; }
.panel-menu-row.form-switch .form-check-input { float: none; margin: 0; }

/* ── panel body, states and KPI (Task 8) ── */
/* min-height: auto, NOT 0. flex: 1 still makes the body take the room the head and
   foot leave, but the automatic minimum size keeps it from being squeezed below its
   own content — which is what let an opened drawer spill out of it. min-height: 0 is
   the usual flex escape hatch for a child that must be allowed to shrink; nothing
   here needs that, because the chart host is sized in pixels by fitHost(). Together
   with .panel's min-height: 100% above, an opened drawer now makes the card taller
   instead of painting over its own footer (measured: 443px of overflow became 0). */
.panel-body { padding: var(--space-3); flex: 1; min-height: auto; }

/* .recommendation-panel (_DashboardPanel.cshtml's text/summary panel wrapper)
   has no rule here on purpose, checked live in both themes: its children are
   headings, small/mb-2 paragraphs and .text-primary bullets, so it already
   inherits --text from .panel-body and the h1-h6/code rules above — adding a
   rule here would only restate that inheritance. */

.panel-state {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--space-2); padding: var(--space-6) var(--space-4);
    text-align: center; min-height: 160px;
}
.panel-state-icon   { font-size: 1.75rem; opacity: .35; }
.panel-state-title  { font-weight: 600; margin: 0; color: var(--text); }
.panel-state-detail { font-size: var(--text-sm); color: var(--text-muted); margin: 0; }
.panel-state--error .panel-state-icon  { color: var(--danger); opacity: .8; }
.panel-state--error .panel-state-title { color: var(--danger); }

/* SQL inspector — one shared treatment so a refreshed (Razor) panel and its
   unrefreshed (JS-streamed) sibling never show two different looks for the
   same block, in either the success or the error branch. */
.panel-sql {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text);
    white-space: pre-wrap;
    word-break: break-all;
}

/* ── panel footer (Task 9): provider badge, total time, raw-data toggle,
   feedback thumbs. Detailed per-stage timings moved behind the ⋮ menu's
   "Performance" item; the complaint chips/comment/said blocks below reuse
   Bootstrap utility classes only, unchanged from before this task. ── */
/* flex-wrap, because .panel is `overflow: hidden` and this row does not fit a
   [quarter] panel: at 3/12 of a 1200px results column the provider badge, the
   timing, the "N rows" toggle and the two feedback thumbs overflow, and what
   overflowed was clipped off the right edge rather than shown. Wrapping moves
   the thumbs onto a second line — .panel-feedback's margin-left: auto still
   pushes them to the right of it — instead of amputating them. The badge also
   gets an ellipsis: wrapping saves a row that is too wide, but not a single
   provider name that is wider than the panel on its own. */
.panel-foot {
    display: flex; align-items: center; gap: var(--space-2);
    flex-wrap: wrap; row-gap: var(--space-1);
    padding: var(--space-2) var(--space-3);
    border-top: 1px solid var(--border);
    font-size: var(--text-xs); color: var(--text-subtle);
}
.panel-badge {
    background: var(--surface-sunken); color: var(--text-muted);
    border-radius: 999px; padding: 2px 8px; font-weight: 600;
    min-width: 0; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* .panel-timing (the "4.2s" beside .panel-badge) also has no rule on purpose:
   .panel-foot already sets --text-xs/--text-subtle, which is exactly the
   plain-text-next-to-a-pill look it renders with today — checked live next
   to .panel-badge above, in both themes. */
.panel-foot-link { border: 0; background: none; color: var(--text-muted); padding: 0; }
.panel-foot-link:hover { color: var(--accent); text-decoration: underline; }
.panel-feedback { margin-left: auto; display: flex; gap: var(--space-2); }

/* "Was this panel right?" is the one thing in the footer we actively want answered,
   but at .panel-action's 26px and --text-subtle it read as decoration rather than a
   control. Bigger, higher contrast, and with a resting border so it looks pressable —
   32px also clears the 24px minimum for a pointer target (WCAG 2.2 SC 2.5.8). */
.panel-feedback .panel-action {
    width: 32px; height: 32px;
    font-size: var(--text-sm);
    color: var(--text-muted);
    border: 1px solid var(--border);
    background: var(--surface-raised);
    transition: color .15s, background .15s, border-color .15s;
}
.panel-feedback .panel-action:hover {
    color: var(--text);
    background: var(--surface-sunken);
    border-color: var(--border-strong);
}
.feedback-up:hover        { color: var(--success) !important; border-color: var(--success) !important; }
.feedback-down:hover      { color: var(--danger)  !important; border-color: var(--danger)  !important; }
.feedback-up.is-chosen    { color: var(--success); background: var(--success-subtle); border-color: var(--success); }
.feedback-down.is-chosen  { color: var(--danger);  background: var(--danger-subtle);  border-color: var(--danger); }

.kpi { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-2); }
/* No .kpi-icon. It was a 40px accent ring with a glyph in it, ahead of the label and the
   figure — and in the tall form it grew to 56px and took the top third of the card to
   draw a circle round a hash. A KPI card has exactly one thing to say and the figure is
   already saying it. */
.kpi-body { flex: 1; min-width: 0; }
.kpi-value-row { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
/* The figure is 1.5rem (24px), not --text-xl: at 20px it read as a little small on its card
   (2026-09-25). The tall form (.kpi--tall, below) has its own size. */
.kpi-value { font-size: 1.5rem; font-weight: 700; letter-spacing: -.02em;
             line-height: 1.1; color: var(--text); }
.kpi-context { font-size: var(--text-xs); color: var(--text-muted); margin-top: 2px; }
.kpi-delta { display: inline-flex; align-items: center; font-size: var(--text-xs);
             font-weight: 600; border-radius: 999px; padding: 1px 7px; }
.kpi-delta--good { color: var(--success); background: var(--success-subtle); }
.kpi-delta--bad  { color: var(--danger);  background: var(--danger-subtle); }
.kpi-delta--flat { color: var(--text-muted); background: var(--surface-sunken); }
/* No "Needs action" pill (.kpi-flag). It lit every tile whose name reads as bad news once its
   figure passed zero, in the warning colours, so a healthy dashboard looked like six warnings. A
   card's one warning is .panel-status in its header. */

/* A one-figure percentage draws as a radial gauge instead of a bare number (chart-theme.js's
   percentKpi). The ring's box is one fixed size, 152px — GAUGE_PX in chart-theme.js, a 133px ring
   drawn in it — never wider than its card; the ring was 72px until it read as too small, and
   "Organize charts" gives gauges rows of their own so a gauge card does not stretch the counters.
   Under the ring, centred, stack the optional value row (the change) and the context line. That is
   one format at every panel size: two formats made the same figure a different-looking card
   depending on what shared its row. kpi-size.js skips gauges, so this card never takes the tall form.
   No padding above or below: the 12px the .kpi card has there went to the ring's box, so the box
   grew from 128px to 152px and the card stayed exactly as tall (measured, 263px). */
.kpi--gauge {
    flex-direction: column; align-items: center; justify-content: center;
    gap: var(--space-1); height: 100%;
    padding-block: 0;
}
/* One size, everywhere — mirrors GAUGE_PX in chart-theme.js, which also writes it before the
   chart is built. A gauge that grew into its row stretched every counter sharing that row. Capped
   at the card's width, so a narrow card gets a smaller ring instead of a clipped one. */
.kpi-gauge-wrap {
    position: relative; flex: none;
    width: min(152px, 100%); aspect-ratio: 1;
}
.kpi-gauge { width: 100%; height: 100%; }

/* skeleton */
.sk { display: block; background: var(--surface-sunken); border-radius: var(--radius-sm);
      animation: sk-pulse 1.4s ease-in-out infinite; }
.sk-icon  { width: 20px; height: 20px; border-radius: var(--radius-sm); }
.sk-title { height: 14px; width: 55%; }
.sk-chart { height: 150px; width: 100%; margin-top: var(--space-3); }
@keyframes sk-pulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }

@media (prefers-reduced-motion: reduce) { .sk { animation: none; } }

/* Table panels: rules between rows only, a quiet header, numbers aligned on the right. It
   replaced Bootstrap's table-bordered plus a table-dark header, which drew a full grid of bright
   lines over every table on a dark panel. TableLayout decides which columns are .num/.col-ref.
   The ink goes through Bootstrap's own variables: 5.3 colours each cell from --bs-table-color and a
   hovered row from --bs-table-hover-color, so a plain `color` on the table reaches neither. */
.panel-table { --bs-table-bg: transparent; --bs-table-color: var(--text); --bs-table-hover-color: var(--text); }
.panel-table > :not(caption) > * > * { border-left: 0; border-right: 0; }
.panel-table > thead > tr > th {
    font-weight: 600; color: var(--text-muted); background: transparent;
    border-bottom: 1px solid var(--border-strong); white-space: nowrap;
}
.panel-table > tbody > tr > td { border-bottom: 1px solid var(--border); }
.panel-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.panel-table .col-ref { color: var(--text-muted); }
.score-bar {
    display: inline-block; width: 40px; height: 6px; margin-right: var(--space-2);
    border-radius: 3px; background: var(--surface-sunken); vertical-align: middle; overflow: hidden;
}
.score-bar > span { display: block; height: 100%; border-radius: 3px; background: var(--accent); }

/* ── Progress strip: the unknown-total state ──────────────────────────────────
   The server announces the number of typed prompts and then departs from it — one
   prompt can expand into several panels. Once every announced panel is in and the
   stream is still open, there is no denominator left to be a fraction of, and the
   bar used to sit pinned at 95% under a label reading "Still generating…": a
   percentage of a total the label had just admitted nobody knows, unmoving for
   every further panel, which reads as a stall rather than as progress.
   Indeterminate gives up the position claim and keeps the motion: a short segment
   sliding the length of the track says "running", and no edge of it lands anywhere
   that could be read as "this far along". Colour comes from the .bg-primary
   Bootstrap already put on the element - nothing new to theme. */
.progress-bar--indeterminate {
    width: 35% !important;
    animation: progress-slide 1.5s ease-in-out infinite;
}
@keyframes progress-slide {
    from { transform: translateX(-110%); }
    to   { transform: translateX(320%); }
}
/* No sliding, and equally no full bar standing in for it — a filled track is the
   one thing this state must never look like. The segment simply parks off-centre,
   where it cannot be read as a percentage either. */
@media (prefers-reduced-motion: reduce) {
    .progress-bar--indeterminate { animation: none; transform: translateX(90%); }
}

/* ── auth + users (Task 12) ── */

/* Login / AccessDenied — pre-authentication screens, rendered by _AuthLayout
   without the app rail. */
.auth-shell {
    min-height: 100vh;
    display: grid;
    place-items: center;
    background: var(--surface);
    padding: var(--space-4);
}
.auth-card {
    width: 100%;
    max-width: 380px;
    background: var(--surface-raised);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
    padding: var(--space-6);
}
.auth-card--center { text-align: center; }
/* A6 — the logo is supplied with its colours baked in, and the "solidrange" wordmark
   inside the colour file is #042c75. On the dark card (--surface-raised #161d28) that
   measures 1.31:1: the mark showed, the name did not.

   This used to be answered with --logo-plate, a light chip painted behind the colour
   logo in dark mode, because recolouring someone's trademark is not ours to do. There
   is now a white version of the artwork from the brand owner, which is the better
   answer by some distance: the logo sits directly on the card, the same way it does in
   light mode, instead of on a light rectangle that reads as a sticker on a dark page.

   Two files rather than one recoloured with CSS filters: the white file is not an
   inversion of the colour one — its cube is white and grey where the colour cube is six
   different blues — so no filter chain reaches it from the other, and any that came
   close would be guessing at a trademark. The plate token is retired with this; nothing
   else used it.

   box-sizing: content-box keeps `height` the logo's height, so the padding grows the
   box around it rather than squeezing the artwork. */
.auth-logo {
    height: 34px; width: auto; display: block;
    box-sizing: content-box;
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    /* The negative side margins cancel the horizontal padding, so the artwork starts on
       the card's content edge — level with "Sign in" below it — in both themes.

       The bottom margin is the gap ABOVE the heading, and it is set here rather than as a
       margin-top on .auth-title because .auth-card is a plain block: adjacent siblings'
       vertical margins collapse, so a margin-top there would not add to this one, it would
       compete with it and the larger of the two would silently win. */
    margin: 0 calc(var(--space-3) * -1) var(--space-5);
}

/* One of the two is always display:none, which is also what keeps the hidden one out of
   the accessibility tree — see the note in Login.cshtml about both carrying the alt. */
.auth-logo--dark  { display: none; }
[data-bs-theme="dark"] .auth-logo--light { display: none; }
[data-bs-theme="dark"] .auth-logo--dark  { display: block; }
.auth-card--center .auth-logo { margin-left: auto; margin-right: auto; }
/* Centred, while the logo above and the form below stay on the content edge. That is the
   ordinary shape of an auth card: the heading addresses you, the fields are things you fill
   in left to right, and centring those would put every label somewhere different.

   In the rule rather than behind a modifier class because both cards that use these are
   meant to read this way — AccessDenied already centres its whole card, so it sees no change
   from this, and Login is the one asking for it. A modifier would exist to be set on every
   caller there will ever be. */
.auth-title    { font-size: var(--text-xl); margin-bottom: var(--space-2); color: var(--text);
                 text-align: center; }
/* margin-bottom is the gap BELOW the heading block, separating it from the first field —
   the counterpart to the logo's bottom margin above it. */
.auth-subtitle { font-size: var(--text-sm); color: var(--text-muted); margin-bottom: var(--space-6);
                 text-align: center; }

.auth-field { margin-bottom: var(--space-4); }
.auth-input-group { position: relative; }
.auth-input-icon {
    position: absolute; left: var(--space-3); top: 50%; transform: translateY(-50%);
    color: var(--text-subtle); pointer-events: none;
}
.auth-input { padding-left: calc(var(--space-3) * 2 + 1rem); }

.auth-submit { width: 100%; margin-top: var(--space-2); }
.auth-submit.loading { pointer-events: none; opacity: .7; }

.auth-denied-icon { font-size: 2.5rem; color: var(--danger); margin-bottom: var(--space-4); display: block; }

/* The loading state (spec 011, FR-019, research R15): the sign-in card's frame with a spinner, from the first paint until a
   screen draws, so a slow answer is never a blank page. The slow sentence is revealed by removing [hidden]. */
.app-loading-spinner { display: block; margin: 0 auto var(--space-4); }
.app-loading-text    { margin-bottom: 0; }
.app-loading-slow    { font-size: var(--text-sm); color: var(--text-muted); margin: var(--space-4) 0 0; text-align: center; }

/* Shared across every asp-validation-summary in the app: the tag helper's
   own markup has no hook for list styling. */
[data-valmsg-summary] ul { list-style: none; margin: 0; padding: 0; }

/* Users screens. min-width: 0 overrides the flex item's default auto, which
   sizes to content (the table's un-breaking email column) rather than the space
   .app-shell actually has — without it the whole page pushed past the viewport
   and scrolled sideways, rail included, instead of just the table scrolling
   inside .users-panel's own overflow-x. Confirmed at 375px: 734px of scrollWidth
   in a 375px viewport before this rule, none after. */
/* align-self: flex-start, because .app-shell is a flex row with the default
   align-items: stretch — so this page, as its only in-flow child beside the rail,
   was stretched to the shell's full min-height: 100vh. .panel's height: 100% (it
   is written for the dashboard grid, where panels in a row must match) then filled
   that, giving the roster a card several hundred pixels taller than its own table
   and the two form screens a mostly-empty box; the page's own padding pushed the
   total past the viewport, so every Users screen also carried a scrollbar that had
   nothing to scroll. Starting the item instead lets all three size to content. */
/* .reviews-* joins each of these selectors rather than restating its rules. The
   correction-review queue and the record detail are the same kind of page as the
   Users roster — a capped column holding cards with tables and one form — and two
   copies of these rules would drift apart the first time either page was touched.
   Only the transcript, the filter row and the pasted screenshot need anything of
   their own; those are the four rules at the end of this block. */
.users-page,
.reviews-page {
    max-width: 960px; padding: var(--space-4) var(--space-5); min-width: 0;
    align-self: flex-start;
    /* Only the cross axis is being started; a flex item still gets no width from
       that, and shrink-to-fit sized the two form screens to their inputs' own
       ~275px. width: 100% (capped by the max-width that was already here) gives
       .users-form-panel the 560px it asks for and leaves the roster, which was
       already hitting the 960 cap, exactly where it was. */
    width: 100%;
}
.users-heading,
.reviews-heading {
    font-size: var(--text-xl); font-weight: 650; color: var(--text);
    display: flex; align-items: center; margin: 0;
}
.users-heading i,
.reviews-heading i { color: var(--accent); }
.users-heading--danger,
.users-heading--danger i { color: var(--danger); }

/* height: auto for the same reason .users-form-panel already carries it — .panel's
   height: 100% belongs to the dashboard grid, not to a roster that should end
   where its last row does. */
.users-panel,
.reviews-panel      { overflow-x: auto; height: auto; }
.users-form-panel,
.reviews-form-panel { max-width: 560px; height: auto; }
.users-form-body,
.reviews-body       { padding: var(--space-6); }

/* A11 — role and lockout checkboxes rendered at Bootstrap's 1em, and this app's
   1em is 14px (--text-md), so they were 14 x 14 targets. WCAG 2.2 SC 2.5.8 asks
   for 24 x 24 CSS px; these are 24 x 24 and the label beside each one is part of
   the same target. Laying the row out as flex replaces .form-check's negative
   text-indent trick, which was measured for a 14px box and would leave a 24px one
   hanging outside the form. */
.users-form-body .form-check,
.reviews-body .form-check {
    display: flex; align-items: center; gap: var(--space-2);
    padding-left: 0; min-height: 1.5rem; margin-bottom: var(--space-2);
}
.users-form-body .form-check-input,
.reviews-body .form-check-input {
    width: 1.5rem; height: 1.5rem; flex: none; margin: 0;
}
.users-form-body .form-check-label,
.reviews-body .form-check-label { margin: 0; cursor: pointer; }

/* The four rules the review screens do not share with Users. */
/* align-items: flex-end so the controls line up on their inputs, not on their
   labels — the day box, the two selects and the buttons all have different
   label heights. */
.reviews-filters { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-end; }
/* Section headings between the cards, smaller than the page's own .reviews-heading. */
.reviews-section { font-size: var(--text-md); font-weight: 650; color: var(--text); margin: 0; }
/* One turn of the transcript. The rule is the spine that makes a five-turn
   conversation read as five turns rather than as one wall. */
.reviews-turn {
    border-left: 3px solid var(--border);
    padding-left: var(--space-3);
    margin-bottom: var(--space-4);
}
/* pre-wrap, because the reader's own words and the assistant's replies contain
   the line breaks the reader typed, and losing them loses what was emphasised. */
.reviews-said { white-space: pre-wrap; margin: 0; }
.reviews-shot {
    max-width: 100%; height: auto;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
}

.data-table { width: 100%; border-collapse: separate; border-spacing: 0; }
.data-table th {
    font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-muted); font-weight: 700; text-align: left;
    padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border);
}
.data-table td { padding: var(--space-3); border-bottom: 1px solid var(--border);
                 color: var(--text); }
.data-table tbody tr:hover { background: var(--surface-sunken); }

/* A sortable header (js/features/diagnostics/table-sort.js): the header's own text as a
   button, so it takes focus and Enter, with the direction drawn from th[aria-sort]. */
.data-table th .diag-sort {
    display: inline-flex; align-items: center; gap: .35em;
    padding: 0; border: 0; background: none; cursor: pointer;
    font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; text-align: inherit;
}
.data-table th .diag-sort:hover,
.data-table th[aria-sort="ascending"] .diag-sort,
.data-table th[aria-sort="descending"] .diag-sort { color: var(--text); }
.data-table th .diag-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.data-table th .diag-sort::after { content: "↕"; font-size: .95em; opacity: .4; }
.data-table th[aria-sort="ascending"] .diag-sort::after { content: "▲"; font-size: .75em; opacity: 1; }
.data-table th[aria-sort="descending"] .diag-sort::after { content: "▼"; font-size: .75em; opacity: 1; }
@media print { .data-table th .diag-sort::after { content: none; } }

/* Access-denied exits. A signed-in user who hits a permissions wall needs a way
   back into the app, not just a link to a login form they are already past. */
.auth-actions {
    display: flex; flex-wrap: wrap; gap: var(--space-2);
    align-items: center; justify-content: center;
}
.auth-actions form { margin: 0; }

/* ── Saved-report drawer (Views/Shared/_SavedReports.cshtml) ───────────────────
   Two sections: the shipped catalogue and the reader's own reports.

   The whole block was rebuilt because the weight was upside down. The section headers
   carried a tinted icon tile, a bold name, an explanatory sub-line, a count pill and a
   chevron — five things — while the reports under them were bare underlined-blue links
   with a line of 11px grey beneath. The chrome was the loudest thing in the drawer and
   the content was the quietest, so the drawer read as a list of two headings that
   happened to have some links stuck under them.

   Inverted: the headers are now quiet dividers, and everything that distinguishes a
   shipped report from a saved one — the icon, the accent — moved down onto the rows,
   where the content is. */

.saved-reports-drawer { width: 360px; }

/* Sticky, because a long catalogue scrolls under it and a reader who has scrolled past
   the "User Reports" header has no way left to tell which section they are in. */
.report-section + .report-section { margin-top: var(--space-2); }

.report-section-toggle {
    position: sticky; top: 0; z-index: 3;
    width: 100%;
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-3) var(--space-4) var(--space-2);
    border: 0;
    background: var(--surface);
    color: var(--text-subtle);
    font-family: var(--font-sans);
    /* Small caps with letter-spacing is the standard "this labels a group" voice, and it
       is what lets the header sit above the rows without competing with them. The old
       header was --text-sm/600 in full --text: the same size and a heavier weight than
       the report names it introduced. */
    font-size: var(--text-xs); font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase;
    text-align: left;
    transition: color .15s;
}
.report-section-toggle:hover { color: var(--text); }
.report-section-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius-sm); }

.report-section-name { flex: 1 1 auto; }

/* A count, not a control. Muted and unpressable-looking on purpose: a bright pill beside
   a button that IS pressable reads as a second button. */
.report-section-count {
    flex: none;
    min-width: 18px; padding: 0 var(--space-2);
    border-radius: 999px;
    background: var(--surface-sunken); color: var(--text-subtle);
    font-size: var(--text-xs); font-weight: 600;
    letter-spacing: 0;
    text-align: center;
}

/* Leads the label rather than trailing it: a disclosure triangle at the START of a
   heading is what a reader already reads as "this opens", and it frees the end of the
   row for the count, which is where a count belongs.

   Points down when open, right when closed. Bootstrap adds .collapsed to the toggle of a
   closed collapse, which is the only state either element carries — so the chevron is
   turned from the button rather than from the panel it controls. */
.report-section-chevron {
    flex: none; width: 9px;
    font-size: .55rem; color: var(--text-subtle);
    transition: transform .2s;
}
.report-section-toggle.collapsed .report-section-chevron { transform: rotate(-90deg); }

/* The reader can turn animation off; the chevron then simply is where it belongs. */
@media (prefers-reduced-motion: reduce) {
    .report-section-chevron { transition: none; }
}

/* ── A product report folder inside System Reports (spec 017) ─────────────────
   A label over its own rows, quieter than the section header above it: sentence case, not
   small caps, and not sticky — the section header stays the one thing that pins. Its count
   is the section count's pill at the folder's scale, counted from the rows below it. */
.report-folder-head {
    display: flex; align-items: center; gap: var(--space-2);
    margin: var(--space-2) 0 var(--space-1);
    padding: 0 var(--space-4);
    color: var(--text-muted);
    font-size: var(--text-xs); font-weight: 600;
}
.report-folder-name { flex: 1 1 auto; }
.report-folder-count {
    flex: none;
    min-width: 18px; padding: 0 var(--space-2);
    border-radius: 999px;
    background: var(--surface-sunken); color: var(--text-subtle);
    font-size: var(--text-xs); font-weight: 600;
    text-align: center;
}

/* ── The rows ─────────────────────────────────────────────────────────────────
   Discrete cards with air around them, not a flush list. Edge-to-edge rows divided by
   hairlines read as a table of data; these are things you open, and they should look
   like it. */
.report-list {
    list-style: none;
    margin: 0; padding: 0 var(--space-3) var(--space-2);
    display: flex; flex-direction: column; gap: var(--space-1);
}

.report-item {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    transition: background .15s, border-color .15s;
}
/* The hover state is the affordance. It is on the LIST ITEM, not the link, because the
   whole row is the target — see .stretched-link below. */
.report-item:hover,
.report-item:focus-within {
    background: var(--surface-raised);
    border-color: var(--border);
}

/* No icon tile on a row. There was one briefly — the accent tile moved down off the
   section header — on the reasoning that it tells a shipped report from a saved one at a
   glance. It does not earn its place: the two are already in separate labelled,
   collapsible sections, so the tile repeated on every row what the header above it
   already said once, and put a block of colour in front of the only thing on the row
   worth reading. The name starts at the left edge now.

   The gap below is what the icon used to occupy in the flex row; with it gone the text
   and the trailing control are the only two children left. */

/* min-width:0 is what lets text-break actually break. A flex child defaults to
   min-width:auto, which refuses to shrink below its content — so a long unbroken report
   name pushed the trailing control off the end of the row instead of wrapping. */
.report-item-text { flex: 1 1 auto; min-width: 0; line-height: 1.3; }

/* --text, not the link colour. Underlined-blue is the voice of a hyperlink in a document;
   these are items in an app's own list, and rendering them as web links is most of why
   the drawer read as unstyled. The colour is kept for hover, where it means something. */
.report-item-name {
    display: block;
    color: var(--text);
    font-size: var(--text-sm); font-weight: 600;
    text-decoration: none;
}
.report-item:hover .report-item-name { color: var(--accent); }
.report-item-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

.report-item-meta {
    display: block;
    font-size: var(--text-xs); color: var(--text-subtle);
    margin-top: 1px;
}
/* Spaced with margins rather than written into the text, so a screen reader does not read
   "middle dot" between the panel count and the date. */
.report-item-dot { margin: 0 .4em; }

/* One fixed slot on the end, carrying whichever control the row has: the delete button on
   a saved report, the chevron on a shipped one, never both. They were stacked here once
   and swapped on hover; nothing stacks now, because nothing is hidden. */
.report-item-actions {
    flex: none;
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
}
.report-item-actions form {
    /* The name carries .stretched-link, which covers the whole positioned <li> — so the
       delete form has to be lifted back above that cover or the link eats its clicks.
       z-index alone is not enough: a positioned ancestor is what z-index applies to,
       hence position. */
    position: relative; z-index: 2;
    margin: 0;
    display: flex; align-items: center; justify-content: center;
}

.report-item-go { font-size: .65rem; color: var(--text-subtle); }

/* ALWAYS visible, never revealed on hover.
   It was hover-only, on the reasoning that a delete button is the rarer act and the only
   one here that cannot be undone, so it should stay out of the way until wanted. That
   reasoning is about restraint and it costs the control its availability: a hidden control
   cannot be found by anyone who does not already know it is there, it is invisible on any
   touch screen (the @media (hover:none) patch that used to sit below is an admission the
   rule does not hold), and on a keyboard it was a button in the tab order that only
   appeared once focus had already reached it.
   Restraint belongs in the STYLING, not in the visibility: it is borderless and drawn in
   --text-subtle like .panel-action, so it is present without reading as the row's main
   control, and it takes --danger only when pointed at. 28px clears the 24px minimum for a
   pointer target (WCAG 2.2 SC 2.5.8), and the confirm dialog is what actually guards the
   act. */
.report-item-delete {
    width: 28px; height: 28px;
    border: 0; background: transparent; border-radius: var(--radius-sm);
    color: var(--text-subtle); font-size: var(--text-xs);
    display: inline-flex; align-items: center; justify-content: center;
    transition: color .15s, background .15s;
}
.report-item-delete:hover,
.report-item-delete:focus-visible {
    color: var(--danger); background: var(--danger-subtle);
}

@media (prefers-reduced-motion: reduce) {
    .report-item, .report-item-go, .report-item-delete, .report-item-name { transition: none; }
}

/* ── Empty states ─────────────────────────────────────────────────────────────
   Where the explanatory sub-line went. It used to sit under every section header as a
   permanent fixture — a sentence the reader re-reads on every visit and needs exactly
   once. Here it is shown only to the reader who has nothing yet, which is the only
   reader it helps, and it can afford to say something useful because it is not competing
   with a list. */
.report-empty {
    margin: 0; padding: var(--space-3) var(--space-4);
    font-size: var(--text-sm); color: var(--text-subtle);
}

.report-empty-state {
    margin: 0 var(--space-3) var(--space-3);
    padding: var(--space-5) var(--space-4);
    border: 1px dashed var(--border);
    border-radius: var(--radius-md);
    text-align: center;
}
.report-empty-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; margin-bottom: var(--space-2);
    border-radius: 999px;
    background: var(--surface-sunken); color: var(--text-subtle);
    font-size: var(--text-sm);
}
.report-empty-title {
    margin: 0;
    font-size: var(--text-sm); font-weight: 600; color: var(--text);
}
.report-empty-hint {
    margin: var(--space-1) 0 0;
    font-size: var(--text-xs); color: var(--text-subtle); line-height: 1.45;
}

/* The toolbar CONTAINER, not just the popup: ApexCharts paints it rgba(255,255,255,.85)
   in its own stylesheet, which is the white box the hamburger sat in on a dark panel.
   Transparent rather than re-tinted — it sits in the panel gutter above the plot, so the
   panel surface behind it is already the right ground, and the icons below are
   token-coloured and legible on it in either theme. */
/* border-COLOR, not border:0. Removing the border changes the toolbar box height,
   which moves the plot area beneath it — it shifted the line and bubble baselines by 4000
   pixels apiece. Making it invisible leaves the box model exactly as the library laid it
   out, so nothing below it moves. */
.apexcharts-toolbar { background: transparent !important; border-color: transparent !important; }

/* Its hover is rgba(15,23,42,.06) — a dark wash, invisible against a dark panel. */
.apexcharts-toolbar .apexcharts-menu-icon:hover,
.apexcharts-toolbar .apexcharts-reset-icon:hover,
.apexcharts-toolbar .apexcharts-zoom-icon:hover,
.apexcharts-toolbar .apexcharts-zoomin-icon:hover,
.apexcharts-toolbar .apexcharts-zoomout-icon:hover,
.apexcharts-toolbar .apexcharts-pan-icon:hover { background: var(--surface-sunken) !important; border-radius: var(--radius-sm); }

/* The hamburger and the zoom/pan icons are drawn as strokes that inherit the SVG's own
   fill, which the library leaves at a fixed light-theme grey. --text-muted keeps them
   legible on both surfaces without competing with the data. */
.apexcharts-toolbar .apexcharts-menu-icon svg,
.apexcharts-toolbar .apexcharts-reset-icon svg,
.apexcharts-toolbar .apexcharts-zoom-icon svg,
.apexcharts-toolbar .apexcharts-zoomin-icon svg,
.apexcharts-toolbar .apexcharts-zoomout-icon svg,
.apexcharts-toolbar .apexcharts-pan-icon svg { fill: var(--text-muted); stroke: var(--text-muted); }
.apexcharts-toolbar .apexcharts-selected svg { fill: var(--accent); stroke: var(--accent); }

/* A chart that drills in says so where the pointer is: a pointer over its marks and a line in its
   tooltip. The sentence that used to sit under every chart said it once per panel, all the time,
   to readers who were not pointing at anything. wireDrillIn (chart-theme.js) sets the flag. */
[data-drillable="true"] .apexcharts-bar-area,
[data-drillable="true"] .apexcharts-pie-area,
[data-drillable="true"] .apexcharts-marker,
[data-drillable="true"] .apexcharts-treemap-rect { cursor: pointer; }
[data-drillable="true"] .apexcharts-tooltip-title::after,
[data-drillable="true"] .apexcharts-tooltip:not(:has(.apexcharts-tooltip-title)) .apexcharts-tooltip-series-group.apexcharts-active::after {
    content: "Click to see rows";
    display: block; margin-top: 2px;
    font-size: var(--text-xs); font-weight: 400; color: var(--text-subtle);
}

/* ── Panel status ──────────────────────────────────────────────────────────────
   A failed panel and a doubted one used to say so in the panel body — a red state card
   and a yellow "Worth a look" banner. Both are one mark in the header now, with the words
   in the Performance drawer behind the ⋮ menu. Token-coloured so both themes are covered
   by the same rule; --warning and --danger are already contrast-tuned per theme
   (TokenContrastTests). */
.panel-status { flex: none; font-size: .85rem; line-height: 1; cursor: help; }
.panel-status--warn  { color: var(--warning); }
.panel-status--error { color: var(--danger); }

/* A system tile's provenance (spec 008): words beside the title, never a glyph, so the status
   mark stays the only icon in the header. Muted on purpose: on a system dashboard every panel
   carries it, and it must not compete with a status. Tokens only, so both themes are covered. */
.tile-marker {
    flex: none;
    font-size: var(--text-xs); font-weight: 600; line-height: 1.4;
    color: var(--text-muted);
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 1px 6px;
    white-space: nowrap;
    cursor: help;
}
/* A narrow card - three columns of a laptop's grid with the composer open is about 240px - gave its title
   24px beside the marker's 85, too little to read a word of it. The card's own width decides, not the
   window's: below 320px the marker goes and the title has the room (spec 013, ECFront the same). */
/* size, not inline-size (15 Oct): a grid cell has a height of its own, so the card's parts can also
   answer to how short it is (the compact rules under .panel-foot's grid block). */
.grid-stack-item-content > .dashboard-panel { container-type: size; }
@container (max-width: 320px) { .tile-marker { display: none; } }

/* The same words, where they are actually readable. */
.panel-status-note {
    font-size: var(--text-xs); line-height: 1.45;
    border-radius: var(--radius-sm);
    padding: var(--space-2);
    margin-bottom: var(--space-2);
}
.panel-status-note--warn  { color: var(--warning); background: var(--warning-subtle); }
.panel-status-note--error { color: var(--danger);  background: var(--danger-subtle); }

/* ── Composer resize handle ────────────────────────────────────────────────────
   Sits over the composer's right border. Transparent until pointed at, so it is a
   6px target without being 6px of visible chrome; --accent while dragging so the
   column being sized is obvious once the pointer has left the handle itself. */
.composer-resizer {
    /* A flex item of the shell, sitting between the composer and the results. The
       negative margin pulls it back over the composer's right border, so it is a 6px
       target that costs no layout width. sticky + 100vh so it stays beside the column
       it resizes however far the page scrolls, matching .app-composer above. */
    flex: none; width: 6px; margin-left: -6px;
    position: sticky; top: 0; height: 100vh; z-index: 5;
    cursor: col-resize; background: transparent;
    transition: background .12s;
}
body[data-rail="collapsed"] .composer-resizer { display: none; }
.composer-resizer:hover,
.composer-resizer:focus-visible,
body.is-composer-resizing .composer-resizer { background: var(--accent); }
.composer-resizer:focus-visible { outline: none; }

/* While dragging, the cursor must not flicker to a text caret over the text the
   pointer happens to cross, and nothing must select. */
body.is-composer-resizing { cursor: col-resize; user-select: none; }
body.is-composer-resizing .app-composer { transition: none; }

@media (max-width: 991.98px) {
    /* Below this the composer is a fixed drawer with a width of its own (see the
       rule above), so there is no column edge to drag. */
    .composer-resizer { display: none; }
}

/* ── Prompt text size ──────────────────────────────────────────────────────────
   The textarea carries Bootstrap's .small, which is where 0.875rem comes from; the
   fallback here repeats it so a reader who never touches the control sees exactly what
   they saw before. An #id beats .small on specificity, so --prompt-font-size wins the
   moment it is set. line-height rides along: bumping the size alone leaves several
   prompts crammed together in a box whose whole job is holding several prompts. */
#rawPrompts {
    font-size: var(--prompt-font-size, 0.875rem);
    line-height: 1.55;
}

/* ── Text direction, per string ────────────────────────────────────────────────
   The composer already sets dir="auto" on what the reader TYPES, and prompts.js sets it
   per line on the suggestion list. Nothing set it on what comes BACK: the layouts hard-code
   <html lang="en">, and an Arabic panel title, table cell or executive summary rendered
   left-to-right — leading punctuation stranded on the wrong side, and any English word or
   number inside an Arabic sentence pushed out of place.

   unicode-bidi: plaintext is the CSS spelling of dir="auto": each element takes its direction
   from its own first strong character. That is what this needs rather than a page-level
   dir="rtl" — one dashboard holds Arabic and English side by side (an Arabic title over a
   table of English category names is ordinary here), so direction belongs to each string
   rather than to the page. It also means neither renderer had to change: Razor and
   dashboard.js emit the same markup they always did.

   Charts are not covered — labels are painted into a canvas, where CSS does not reach. */
/* A summary bullet in Arabic (summary.js gives each line dir="auto"): its row runs right to left, the
   icon at the right, so the chevron points the way the reading goes. */
.recommendation-panel > .d-flex:dir(rtl) > .fa-circle-chevron-right { transform: scaleX(-1); }

.panel-title,
.panel-heading,
.panel .recommendation-panel,
.panel .recommendation-panel h5,
.panel .recommendation-panel h6,
.panel .recommendation-panel p,
.panel .recommendation-panel span,
.panel table th,
.panel table td,
.kpi-context,
.kpi-value {
    unicode-bidi: plaintext;
}

/* ── Prompt readout ───────────────────────────────────────────────────────────
   What the composer says each prompt will do, under the textarea.

   Replaced a static help text listing delimiters and width hints. It sits collapsed at one
   line so the rail costs the same at rest as it did before, and the rules it used to state
   are now shown against the reader's own prompts instead.

   Every colour is a token. This strip reports trouble, and a hard-coded amber is the thing
   that disappears into a dark background — the panel status mark learnt that already. */
.prompt-readout {
    margin-top: .4rem;
    font-size: .78rem;
    color: var(--text-muted, #6c757d);
}

.prompt-readout-summary {
    cursor: pointer;
    list-style: revert;          /* keep the native disclosure triangle */
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .15rem 0;
    user-select: none;
}
.prompt-readout-summary:hover { color: var(--text-body, #212529); }

.prompt-readout-count { display: inline-flex; align-items: center; gap: .3rem; }

.prompt-readout-body {
    /* A nine-prompt run is nine rows. Capped and scrollable so expanding the list pushes
       Generate down a little rather than off the end of the rail — the same lesson the
       suggestion list above it was moved for. */
    max-height: 14rem;
    overflow-y: auto;
    margin-top: .25rem;
    padding-inline-start: .1rem;
}

.prompt-readout-row {
    display: flex;
    align-items: flex-start;
    gap: .4rem;
    padding: .3rem 0;
    border-top: 1px solid var(--border-subtle, rgba(128, 128, 128, .18));
}
.prompt-readout-row:first-child { border-top: 0; }

.prompt-readout-num {
    flex: 0 0 auto;
    min-width: 1.1rem;
    text-align: end;
    opacity: .6;
    font-variant-numeric: tabular-nums;
}

.prompt-readout-main { flex: 1 1 auto; min-width: 0; }

.prompt-readout-prompt {
    color: var(--text-body, #212529);
    /* One line per prompt, whatever its length — the list is a summary, and the prompt
       itself is legible in the box directly above. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.prompt-readout-facts { opacity: .85; }

/* The hedge, said in the styling as well as the words: a shape the code forces reads as
   plainly as the prompt it describes, and one the model may yet choose does not. */
.prompt-readout-facts--certain { opacity: 1; }
.prompt-readout-facts--unknown { font-style: italic; opacity: .7; }

.prompt-readout-note {
    display: flex;
    align-items: baseline;
    gap: .35rem;
    margin-top: .15rem;
}
.prompt-readout-note--warn { color: var(--bs-warning-text-emphasis, #997404); }
.prompt-readout-note--info { color: var(--text-muted, #6c757d); }

.prompt-readout-dot {
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-radius: 50%;
    display: inline-block;
}
.prompt-readout-dot--warn { background: var(--bs-warning, #ffc107); }
.prompt-readout-dot--info { background: var(--bs-info, #0dcaf0); }

/* The row dot sits with the text, not the first line's baseline. */
.prompt-readout-row > .prompt-readout-dot { margin-top: .35rem; }

/* ── Palette group heading ────────────────────────────────────────────────────
   The // list holds two kinds of entry that must not be mistaken for each other: twelve
   chart types the code forces, and two shapes of answer that depend on what the query
   returns. This is the line between them. role="presentation", so a screen reader reads it
   as the label it is rather than announcing a fourteenth option. */
.prompt-palette-heading {
    padding: .35rem .5rem .15rem;
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted, #6c757d);
    border-top: 1px solid var(--border-subtle, rgba(128, 128, 128, .18));
    background: var(--surface-sunken, transparent);
}
.prompt-palette-heading:first-child { border-top: 0; }

/* ── The suggestion list, and the palette that floats ─────────────────────────
   The cap and the scroll moved here out of a style attribute on the element: the palette
   sets its own height from the room it measures, and clearing those inline styles again
   afterwards would have taken the attribute's values with them — leaving the library list
   uncapped for the rest of the session. */
#promptSuggestions {
    max-height: 11rem;
    overflow-y: auto;
    /* What .rounded, .mt-1 and .shadow-sm used to do from the element. Here so the floating
       rule below can override them, which a utility class carrying !important never allows. */
    margin-top: .25rem;
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-sm);
}

/* Only the palette. The library list stays in the normal flow below the box, where it was
   put after a version that floated DOWNWARD covered the Generate button and swallowed the
   click. This one opens upward, over the tenant dropdown — nothing anyone reaches for in
   the middle of a sentence.

   z-index above Bootstrap's dropdown (1000) and sticky (1020), below its modal backdrop
   (1040): a modal open over the composer should cover this, not sit under it.

   A layer, and styled to read as one: rounder than the form controls underneath it, a
   stronger border, and the app's largest elevation so the shadow falls across the textarea
   it overlaps. Width and position come from floatOverBox, which sizes it narrower than the
   box on purpose — matched to the box it read as another section of the form.

   Tokens, not literals: --shadow-lg and --surface-raised both have dark-theme values, and a
   hard-coded rgba shadow is the thing that disappears on a dark background. */
#promptSuggestions.is-floating {
    z-index: 1030;
    margin-top: 0;
    border-radius: var(--radius-md);
    border-color: var(--border-strong);
    background: var(--surface-raised);
    box-shadow: var(--shadow-lg);
}

/* Bootstrap gives the first and last rows their own corners from the parent; without this
   the square row corners paint over the rounded container at the top and bottom. */
#promptSuggestions.is-floating > :first-child { border-top-left-radius: inherit; border-top-right-radius: inherit; }
#promptSuggestions.is-floating > :last-child  { border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }

/* ── A single figure in a tall panel ──────────────────────────────────────────
   The card is 65px of content; on a mixed dashboard its panel body is 391-509px. The small
   layout is right when a row holds nothing but cards and the body is barely taller than the
   card — and wrong everywhere else, where the answer sat in a corner of its own panel.

   kpi-size.js adds .kpi--tall from a measured height. Not a CSS height query: that needs
   container-type: size, which is circular here — the row takes its height from the panel,
   the panel would take its height from the row, and a row of only cards has nobody left to
   ask. */
.kpi--tall {
    height: 100%;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    gap: var(--space-3);
}

/* The figure stays above its context line, as it is in the markup — the reading order a
   screen reader gets ("88, of 130 · 68%") is the one that makes sense, and reversing the
   two visually would only put them out of step with it. */
.kpi--tall .kpi-body      { flex: none; }
.kpi--tall .kpi-context   { font-size: var(--text-sm); margin-top: var(--space-2); }
.kpi--tall .kpi-value-row { justify-content: center; }

/* Up to twice the small card's figure: 48px at most. It was 56px, near three times the old 20px,
   and read as a little big (2026-09-25). Clamped against the viewport as well so the figure
   cannot outgrow a short window on a laptop, where the panel is tall only in proportion. */
.kpi--tall .kpi-value {
    font-size: clamp(2rem, 6vh, 3rem);
    line-height: 1;
}

/* .kpi--tall .kpi-icon is gone with the ring itself. The tall card is now the label and
   the figure, centred, with nothing above them. */

/* ── Diagnostics console ──────────────────────────────────────────────────────
   The app's first tabbed screen. Built only on tokens that exist, so the dark
   block in tokens.css carries it without a second set of rules here. */
.diagnostics-heading {
    font-size: var(--text-2xl);
    font-weight: 700;
    color: var(--text);
    margin: 0;
}

.diagnostics-window {
    display: flex;
    align-items: center;
}

/* @RenderBody() is a direct flex child of .app-shell, so a page wrapper that declares no
   layout gets none: it shrink-to-fits instead of filling, and flex's default min-width: auto
   lets a wide table push it past the viewport, clipping its own left edge under the rail.
   .users-page and .reviews-page above solve this with a capped 960px column; this console is
   the opposite kind of page — eight-column tables and a KPI strip — so it takes flex: 1 and
   no cap. min-width: 0 is the half that stops the clipping: without it a table wider than the
   space available wins the negotiation and the panel overflows rather than scrolling. */
.diagnostics-page {
    flex: 1;
    min-width: 0;
    padding: var(--space-4) var(--space-5);
}

.diagnostics-tabs {
    display: flex;
    gap: var(--space-1);
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-4);
    overflow-x: auto;
}

.diagnostics-tab {
    appearance: none;
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 600;
    padding: var(--space-2) var(--space-3);
    white-space: nowrap;
    cursor: pointer;
}

.diagnostics-tab:hover { color: var(--text); }

/* An inset ring, not the global one. base.css's :focus-visible draws at outline-offset: 2px,
   i.e. outside the button's box — and .diagnostics-tabs above sets overflow-x: auto, which
   makes overflow-y compute to auto as well (a non-visible value on one axis forces the
   other). The strip therefore clips in both directions, and since the tabs fill its height
   the ring lost its top and bottom edges and rendered as two vertical brackets either side
   of the tab. Pulling the outline inside the button puts it out of reach of any ancestor's
   overflow. Same colour and width as the global ring, so the contrast base.css argues for
   (5.25:1 light, 6.85:1 dark against --surface) is unchanged; same treatment as
   .report-section-toggle, which is inset for this reason. */
.diagnostics-tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
    border-radius: var(--radius-sm);
}

.diagnostics-tab.is-active {
    color: var(--accent);
    border-bottom-color: var(--accent);
}

/* Panes are hidden rather than removed: a loaded tab stays in the DOM, so switching
   back to it costs nothing and never refetches. */
.diagnostics-pane { display: none; }
.diagnostics-pane.is-active { display: block; }

.diag-grid {
    display: grid;
    gap: var(--space-4);
}

/* The console's panels are bare .panel cards. A dashboard card is inset by its .panel-head and
   .panel-body; these have neither, so a section title, a note or a filter row sat on the card's
   edge, and the rounded corner — clipped by .panel's overflow: hidden — cut into its letters. */
.diagnostics-page .panel { padding: var(--space-4); }

.diag-strip {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    gap: var(--space-4);
    overflow-x: auto;
}

.diag-stat {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
}

.diag-stat-value {
    font-size: var(--text-xl);
    font-weight: 700;
    color: var(--text);
    line-height: 1.1;
}

.diag-stat-unit {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
    margin-left: .15em;
}

.diag-stat-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    margin-top: var(--space-1);
}

/* Diagnostics on a phone. The tables here are wider than a phone-width panel, and .panel is
   overflow:hidden, so the right-hand columns are clipped with no way to reach them (R-01); and
   the seven health stats, at flex:1/min-width:0, squeeze to illegibility rather than scroll
   (R-02). Scoped to the diagnostics grid so dashboard panels are untouched. */
@media (max-width: 991px) {
    .diag-grid .panel { overflow-x: auto; }
    .diag-strip .diag-stat { flex: 0 0 auto; min-width: 7rem; }
}

.diag-section-title {
    font-size: var(--text-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    margin: 0 0 var(--space-3);
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-3);
}

.diag-section-note {
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
    color: var(--text-subtle);
}

.diag-note {
    font-size: var(--text-xs);
    margin-bottom: var(--space-3);
}

/* Capped on the list, not on .diag-reason-text. These reasons are long technical prose with
   SQL inline, and .panel has no max-width — on a wide monitor a line ran past 250 characters,
   roughly three times a readable measure. Constraining the text alone would fix that but leave
   the proportional bars, which li::before draws across the whole row, still running the full
   panel width; text on one measure and its own bar on another reads worse than neither. The
   cap goes here so the bar and its label share a measure and the thing reads as the chart it
   is. In ch so it tracks the font rather than a guessed pixel width. */
.diag-reasons {
    list-style: none;
    margin: 0;
    padding: 0;
    max-width: 100ch;
}

/* position: relative and overflow: hidden used to be here, to contain and clip an absolutely
   positioned bar. Both went with it. */
.diag-reasons li {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--border);
}

.diag-reasons li:last-child { border-bottom: 0; }

.diag-reason-count {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--text);
    min-width: 2.5rem;
    text-align: right;
}

.diag-reason-text {
    color: var(--text-muted);
    word-break: break-word;
}

/* The leading action column. Sized to its buttons and not allowed to wrap, so the column a
   reader aims at stays a fixed target as they scan down, and a long prompt in the next column
   cannot squeeze it. */
.diag-col-action {
    width: 1%;
    white-space: nowrap;
    vertical-align: middle;
}

/* ── Pager ──
   Sits under a grid, inside the same panel. The count line and the controls are separated to
   the two ends rather than grouped: the count is what a reader checks constantly ("is this
   all of it?") and the controls are what they reach for occasionally. */
.diag-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding-top: var(--space-3);
    margin-top: var(--space-3);
    border-top: 1px solid var(--border);
}

.diag-pager-count { font-size: var(--text-sm); color: var(--text-muted); }
.diag-pager-count strong { color: var(--text); font-variant-numeric: tabular-nums; }

.diag-pager-links { display: flex; align-items: center; gap: var(--space-2); }

.diag-pager-link {
    display: inline-flex; align-items: center; gap: var(--space-1);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm); font-weight: 600;
    color: var(--text); text-decoration: none;
}

.diag-pager-link:hover { background: var(--surface-sunken); color: var(--text); }

/* Rendered as a span rather than a disabled anchor, so this is appearance only — there is
   no focusable element here to also have to neutralise. */
.diag-pager-link.is-disabled {
    color: var(--text-muted);
    opacity: .5;
    border-color: transparent;
}

.diag-pager-position {
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    padding: 0 var(--space-1);
}

/* ── Sparklines ── */
.diag-sparkline {
    display: block;
    width: 100%;
    height: 24px;
    margin-top: var(--space-1);
    overflow: visible;
}

.diag-sparkline-line {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.5;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}

.diag-sparkline-area {
    fill: var(--accent);
    opacity: 0.12;
}

.diag-sparkline-dot {
    fill: var(--accent);
}

/* ── Reason bars ── */
/* There is no proportional bar on these rows. There was one — first a tinted block behind the
   whole row, then a 2px accent segment on the divider — and it is gone on purpose. The counts
   are already sorted descending and printed in tabular figures, so the ranking a bar would
   convey is right there in the first column; the bar only added a coloured artefact to rows of
   dense technical prose. If a visual weighting is ever wanted here, it belongs beside the
   count, not behind or under the text. */

.diag-verdicts {
    display: flex;
    gap: var(--space-3);
}

.diag-verdict {
    font-size: var(--text-xs);
    font-weight: 700;
    padding: .2rem .55rem;
    border-radius: var(--radius-sm);
}

.diag-verdict--up   { background: var(--success-subtle); color: var(--success); }
.diag-verdict--down { background: var(--danger-subtle);  color: var(--danger); }
.diag-verdict--warn { background: var(--warning-subtle); color: var(--warning); }

/* ── Runs tab ── */
.diag-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    align-items: flex-end;
}

.diag-filters input[type="text"],
.diag-filters input:not([type]),
.diag-filters select { min-width: 12rem; }

.diag-filter-checks {
    display: flex;
    gap: var(--space-3);
    font-size: var(--text-sm);
    color: var(--text-muted);
    padding-bottom: .4rem;
}

.diag-filter-checks label {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    white-space: nowrap;
}

.diag-filter-actions {
    display: flex;
    gap: var(--space-2);
}

.diag-nowrap { white-space: nowrap; }

/* A prompt is arbitrary reader text and will otherwise set the table's width. */
.diag-prompt {
    max-width: 28rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── The slide-over ── */
.diag-slideover-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, .45);
    display: flex;
    justify-content: flex-end;
    z-index: 1060;
}

.diag-slideover {
    background: var(--surface-raised);
    width: min(46rem, 100%);
    height: 100%;
    display: flex;
    flex-direction: column;
    box-shadow: var(--shadow-lg);
}

.diag-slideover-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.diag-slideover-head h2 {
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text);
    margin: 0;
}

.diag-slideover-close {
    appearance: none;
    background: transparent;
    border: 0;
    color: var(--text-muted);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

.diag-slideover-close:hover { color: var(--text); }

.diag-slideover-body {
    padding: var(--space-4);
    overflow-y: auto;
    flex: 1;
}

/* ── Run detail ── */
.diag-detail-block { margin-bottom: var(--space-5); }
.diag-detail-block:last-child { margin-bottom: 0; }

.diag-detail-prompt {
    font-size: var(--text-lg);
    color: var(--text);
    margin-bottom: var(--space-3);
}

.diag-subtitle {
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--text);
    margin: var(--space-4) 0 var(--space-2);
}

.diag-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--space-2) var(--space-4);
    margin: 0;
}

.diag-facts dt {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    font-weight: 700;
}

.diag-facts dd {
    margin: 0;
    color: var(--text);
    word-break: break-word;
}

.diag-danger { color: var(--danger); }

.diag-sql {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    margin: var(--space-3) 0 0;
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text);
    white-space: pre-wrap;
    word-break: break-word;
}

.diag-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
}

.diag-chip {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .15rem .5rem;
    font-size: var(--text-xs);
    color: var(--text);
}

.diag-chip-note {
    color: var(--text-muted);
    margin-left: .35em;
}

/* The screenshot a reader attached to a note, under the Feedback tab's comment; opens full size. */
.diag-feedback-shot { display: block; margin-top: var(--space-1); }
.diag-feedback-shot img {
    max-width: 120px; max-height: 72px; object-fit: cover;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
}

.diag-link-note { margin-bottom: var(--space-3); }

/* The system prompt runs to ~23 KB. Collapsed by default so it does not bury the rest of
   the panel, but present — the point of the section is that you can read it. */
.diag-prompt-block {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-3);
    background: var(--surface-sunken);
}

.diag-prompt-block > summary {
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--text);
}

.diag-prompt-block[open] > summary { margin-bottom: var(--space-3); }

.diag-prompt-block .diag-sql {
    max-height: 26rem;
    overflow-y: auto;
    background: var(--surface-raised);
}

/* A handled complaint stays readable but recedes, so what is left to do is what stands out. */
.diag-row-handled { opacity: .55; }

/* ── Drafts tab actions ── */
.diag-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-5);
    margin-bottom: var(--space-3);
}

.diag-action { max-width: 24rem; }

.diag-action-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

.diag-action-row input { width: 5.5rem; }

.diag-action-result {
    font-size: var(--text-sm);
    min-height: 1.25rem;
}

/* ── Prompt detail ── */
.diag-variant { margin-bottom: var(--space-3); }

.diag-variant-count {
    display: inline-block;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* Header timestamp — Views/Dashboard/Index.cshtml's #dataAsOf, set by dashboard.js's showDataAsOf. */
.data-as-of { font-size: var(--text-xs); color: var(--text-subtle); margin-top: 2px; }

/* Header report name — Views/Dashboard/Index.cshtml's #openReport: the system dashboard or saved
   report the page was opened from, between the page title and "Data as of". Hidden by
   dashboard.js's clearDashboard. The System tag is the drawer's count pill (.report-section-count),
   outlined: the drawer sits on --surface-raised, this on --surface, and in dark the sunken fill is
   all but the page's own colour, which left the tag reading as loose grey text. */
.open-report { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
.open-report-name { color: var(--text); font-size: var(--text-md); font-weight: 600; overflow-wrap: anywhere; }
.open-report-badge {
    border: 1px solid var(--border); border-radius: 999px; padding: 0 var(--space-2);
    background: var(--surface-sunken); color: var(--text-subtle);
    font-size: var(--text-xs); font-weight: 600; line-height: 1.6;
}

/* The dashboard PDF's heading — dashboard.js's exportReportTitle, put in front of the panels for
   the length of the export only (the page header is not exported). Exports render light, so these
   read the light tokens. Inset by half the grid's gutter, which is where the panel cards start. */
.export-title { margin: 0 0 var(--space-4); padding: 0 calc(var(--bs-gutter-x, 1rem) * .5); }
.export-title-name { color: var(--text); font-size: var(--text-2xl); font-weight: 700; line-height: 1.2; }
.export-title-meta { color: var(--text-subtle); font-size: var(--text-sm); margin-top: var(--space-1); }

/* Prompt library: one fold per module, closed until opened or until a search matches in it. */
.prompt-lib-group { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: var(--space-2); background: var(--surface-raised); overflow: hidden; }
.prompt-lib-group > summary { display: flex; align-items: center; gap: var(--space-2); padding: .5rem .75rem; cursor: pointer; list-style: none; font-size: var(--text-sm); font-weight: 600; color: var(--text); user-select: none; }
.prompt-lib-group > summary::-webkit-details-marker { display: none; }
.prompt-lib-group > summary::before { content: ""; width: .45rem; height: .45rem; flex: none; border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted); transform: rotate(-45deg); transition: transform .15s ease; }
[dir="rtl"] .prompt-lib-group > summary::before { transform: rotate(135deg); }
.prompt-lib-group[open] > summary::before { transform: rotate(45deg); }
.prompt-lib-group > summary:hover { background: var(--surface-sunken); }
.prompt-lib-group[open] > summary { border-bottom: 1px solid var(--border); }
.prompt-lib-name { flex: 1; min-width: 0; }
.prompt-lib-count { flex: none; min-width: 1.5rem; padding: 0 .4rem; border-radius: 999px; background: var(--accent-subtle); color: var(--accent); font-size: var(--text-xs); line-height: 1.25rem; text-align: center; }
.prompt-lib-item { padding: .45rem .75rem .45rem 1.6rem !important; border-color: var(--border); }
.prompt-lib-item > .fw-semibold { font-weight: 500 !important; }
.prompt-lib-item > div:empty { display: none; }
@media (prefers-reduced-motion: reduce) { .prompt-lib-group > summary::before { transition: none; } }
/* ── The executive summary card (spec 015; laid out again 17 Oct at the user's request): summary-card.js renders it in
   the summary panel's body. Where you stand leads, what drives it follows, the decisions are the body of the card, and
   what supports them is folded underneath, one line each with its count; the question box closes it. Tokens only. */
.summary-card { display: flex; flex-direction: column; gap: var(--space-4); }
.summary-banner { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-sunken); font-size: var(--text-sm); }
.summary-banner--failed { background: var(--danger-subtle); border-color: var(--danger); }
.summary-banner--stale { background: var(--warning-subtle); }
.summary-upgrade { margin-bottom: var(--space-3); }
.summary-toolbar { display: flex; align-items: center; gap: var(--space-2); }
/* When it was written at the start, Regenerate and the pop-up button together at the end. */
.summary-toolbar > .summary-muted { margin-inline-end: auto; }
.summary-muted { margin: 0; color: var(--text-muted); font-size: var(--text-xs); }
.summary-icon { margin-inline-end: .35em; }
.summary-overline { display: flex; align-items: center; gap: var(--space-2); margin: 0; color: var(--text-muted); font-size: var(--text-xs); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
.summary-card[lang="ar"] .summary-overline { font-size: var(--text-sm); letter-spacing: 0; text-transform: none; }
.summary-count { display: inline-flex; justify-content: center; min-width: 1.6em; padding: 0 6px; border-radius: 999px; background: var(--surface-sunken); color: var(--text-muted); font-size: var(--text-xs); font-weight: 600; letter-spacing: 0; text-transform: none; }

/* Every section folds by its name (the user, 17 Oct: "make each section collapsable"), the chevron at the end of the
   line, as the folded rows below do. The chevron is drawn with physical borders: logical ones turn it sideways in an
   Arabic card. */
.summary-fold-head { display: flex; align-items: center; gap: var(--space-2); border-radius: var(--radius-sm); cursor: pointer; list-style: none; }
.summary-drivers > .summary-fold-head, .summary-decisions-section > .summary-fold-head { padding-inline-end: var(--space-3); }
.summary-fold-head::-webkit-details-marker, .summary-more-head::-webkit-details-marker { display: none; }
.summary-fold[open] > .summary-fold-head { margin-bottom: var(--space-2); }
.summary-fold-head:hover .summary-overline { color: var(--text); }
.summary-fold-head:focus-visible, .summary-more-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.summary-fold-head::after, .summary-more-head::after { content: ''; flex: none; margin-inline-start: auto; width: .45em; height: .45em; border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted); transform: translateY(-25%) rotate(45deg); transition: transform .15s; }
.summary-fold[open] > .summary-fold-head::after, .summary-more-item[open] > .summary-more-head::after { transform: translateY(25%) rotate(-135deg); }
@media (prefers-reduced-motion: reduce) { .summary-fold-head::after, .summary-more-head::after { transition: none; } }

/* Where you stand: the summary's one paragraph that has to be read, so it reads first and larger. */
.summary-lead { padding: var(--space-3) var(--space-4); border-inline-start: 3px solid var(--accent); border-radius: var(--radius-md); background: var(--accent-subtle); }
.summary-lead p { margin: 0 0 var(--space-1); color: var(--text); font-size: var(--text-md); line-height: 1.55; }
.summary-lead p:last-child { margin-bottom: 0; }
.summary-section p { margin: 0 0 var(--space-1); }
.summary-list { margin: 0; padding-inline-start: 1.25rem; }
.summary-drivers .summary-list li { margin-bottom: var(--space-1); }
.summary-drivers .summary-list li::marker { color: var(--accent); }

/* The decisions: a grid of cards, the urgency on each card's leading edge. */
.summary-decisions { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
.summary-decision { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--border); border-inline-start: 4px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface-raised); box-shadow: var(--shadow-sm); }
.summary-decision--high { border-inline-start-color: var(--danger); }
.summary-decision--medium { border-inline-start-color: var(--warning); }
.summary-decision-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.summary-urgency { padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px; font-size: var(--text-xs); font-weight: 600; color: var(--text-muted); }
.summary-urgency--high { color: var(--danger); background: var(--danger-subtle); border-color: var(--danger); }
.summary-urgency--medium { color: var(--text); background: var(--warning-subtle); border-color: var(--warning); }
.summary-subject { padding: 0 6px; border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-muted); font-size: var(--text-xs); }
.summary-action { font-size: var(--text-md); line-height: 1.35; }
.summary-reason { margin: 0; color: var(--text-muted); font-size: var(--text-sm); }
.summary-decision-foot { display: flex; flex-direction: column; gap: var(--space-2); margin-top: auto; padding-top: var(--space-2); border-top: 1px solid var(--border); font-size: var(--text-sm); }
.summary-evidence { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
.summary-label { color: var(--text-muted); font-size: var(--text-xs); }
.btn.summary-chip { padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-sunken); color: var(--text); font-size: var(--text-xs); }
.btn.summary-chip:hover, .btn.summary-chip:focus-visible { border-color: var(--accent); background: var(--accent-subtle); color: var(--text); }
.summary-nexts { display: flex; flex-direction: column; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
.summary-next { display: grid; grid-template-columns: auto 1fr; align-items: baseline; column-gap: var(--space-2); }
.summary-next > .btn { grid-column: 2; justify-self: start; margin: 0; font-size: var(--text-xs); }
.summary-next-icon { color: var(--text-muted); font-size: var(--text-xs); }
.summary-mark-actions, .summary-mark-form, .summary-mark { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
.summary-mark-form input { flex: 1 1 12rem; max-width: 22rem; }
.summary-mark { align-self: flex-start; padding: 2px var(--space-2); border-radius: var(--radius-sm); background: var(--surface-sunken); }
.summary-mark--accepted { background: var(--success-subtle); }

/* Every card the same shape (the user, 18 Oct: "more readable and tidy"): the action and its reason are cut to a few
   lines, and the expand button at the head's end opens the whole decision in a pop-up. */
.summary-decision .summary-action { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.summary-decision .summary-reason { display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.5; }
.btn.summary-expand { margin-inline-start: auto; padding: 2px 6px; border: 1px solid transparent; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; }
.btn.summary-expand:hover, .btn.summary-expand:focus-visible { border-color: var(--border); background: var(--surface-sunken); color: var(--text); }

/* The decision opened large: one column, read top to bottom - urgency and subject, the action as a heading, the reason
   at reading size, then what it rests on, the next questions and its mark, each set apart. */
.summary-dialog { width: min(720px, calc(100vw - 2 * var(--space-4))); max-height: min(85vh, 900px); padding: 0; border: 1px solid var(--border); border-inline-start: 5px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-raised); color: var(--text); box-shadow: var(--shadow-lg); overflow: hidden; }
.summary-dialog[open] { display: flex; flex-direction: column; }
.summary-dialog::backdrop { background: rgb(0 0 0 / .55); }
.summary-dialog--high { border-inline-start-color: var(--danger); }
.summary-dialog--medium { border-inline-start-color: var(--warning); }
.summary-dialog-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
.summary-dialog-position { color: var(--text-muted); font-size: var(--text-xs); }
.summary-dialog-nav { display: flex; gap: var(--space-1); margin-inline-start: auto; }
.btn.summary-dialog-btn { width: 2rem; height: 2rem; padding: 0; border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); }
.btn.summary-dialog-btn:hover:not(:disabled), .btn.summary-dialog-btn:focus-visible { background: var(--surface-sunken); border-color: var(--accent); }
.btn.summary-dialog-btn:disabled { opacity: .4; }
[dir="rtl"] .summary-flip { transform: scaleX(-1); }
/* The whole summary in the pop-up: wide, so the decisions sit three or four to a row, and tall, so the reader scrolls
   once inside it rather than inside a grid tile. Its card on the grid says where it went. */
.summary-dialog--summary { width: min(1200px, calc(100vw - 2 * var(--space-4))); height: min(90vh, 1000px); max-height: min(90vh, 1000px); border-inline-start-width: 1px; }
.summary-dialog-title { margin: 0; font-size: var(--text-lg); font-weight: 700; outline: none; }
.summary-dialog-summary { padding: var(--space-4) var(--space-5); }
.btn.summary-dialog-back { display: inline-flex; align-items: center; padding: 2px var(--space-2); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); font-size: var(--text-sm); }
.btn.summary-dialog-back:hover, .btn.summary-dialog-back:focus-visible { border-color: var(--accent); background: var(--surface-sunken); }
.btn.summary-open-summary { padding: 2px 8px; }
.summary-in-popup { display: flex; align-items: center; margin: 0; padding: var(--space-3); }
.summary-dialog-body { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-5); overflow-y: auto; }
/* The heading takes the focus as the pop-up opens (autofocus), so a reader starts at the top and no button wears a ring. */
.summary-dialog-action { margin: 0; font-size: var(--text-xl); font-weight: 700; line-height: 1.4; outline: none; }
.summary-dialog-reason { margin: 0; color: var(--text); font-size: var(--text-lg); line-height: 1.7; }
.summary-dialog-foot { gap: var(--space-4); margin: 0; padding-top: var(--space-4); font-size: var(--text-md); }
.summary-dialog-foot .summary-label { font-size: var(--text-sm); font-weight: 600; }
.summary-dialog-foot .btn.summary-chip { padding: 3px 10px; font-size: var(--text-sm); }
.summary-dialog-foot .summary-nexts { gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); background: var(--surface-sunken); }
.summary-dialog-foot .summary-next-icon, .summary-dialog-foot .summary-next > .btn { font-size: var(--text-sm); }

/* What supports the decisions, folded: one row each, with its count. */
.summary-more { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
.summary-more-item + .summary-more-item { border-top: 1px solid var(--border); }
.summary-more-head { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); cursor: pointer; list-style: none; font-size: var(--text-sm); font-weight: 600; }
.summary-more-head:hover { background: var(--surface-sunken); }
.summary-more-body { padding: 0 var(--space-3) var(--space-3); font-size: var(--text-sm); }
.summary-changes { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.summary-change { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2); padding: var(--space-1) 0; border-bottom: 1px dashed var(--border); }
.summary-change:last-child { border-bottom: 0; }
.summary-change-figures { white-space: nowrap; }
.summary-change--worse .summary-delta { color: var(--danger); font-weight: 600; }
.summary-unverified, .summary-notice { margin: 0; color: var(--danger); font-size: var(--text-sm); }

/* The question box closes the card, on its own surface. */
.summary-ask { padding: var(--space-3); border-radius: var(--radius-md); background: var(--surface-sunken); }
.summary-ask-row { display: flex; gap: var(--space-2); }
.summary-answer { margin-top: var(--space-2); padding: var(--space-2) var(--space-3); border-inline-start: 3px solid var(--accent); border-radius: var(--radius-sm); background: var(--surface-raised); }
.summary-answer--refused { border-inline-start-color: var(--danger); }
.dashboard-panel.summary-evidence-flash > .panel { outline: 3px solid var(--accent); outline-offset: 2px; }
