/**
 * Report Manager — Shared Design System
 * Single source of truth for all report templates & detail pages.
 *
 * TABLE OF CONTENTS
 * ─────────────────────────────────────────────────────────────────
 *  1. Design Tokens (CSS custom properties)
 *  2. Global Utilities
 *  3. Report Toolbar
 *     3a. Filter Chips
 *     3b. Chip Scroll Wrap + Fade
 *     3c. Action Icon Buttons
 *     3d. Toolbar Layout & Responsive
 *  4. Mobile Cards  (mobile-card pattern)
 *  5. Contact Mobile Cards  (cmc pattern — detail pages)
 *  6. Data Tables
 *     6a. Sticky headers + sticky first column
 *     6b. Row tints
 *     6c. Horizontal scroll wrap + fade
 *  7. Confirm Modal
 *  8. Cards & Snapshots (account detail)
 *  9. Health Circle
 * 10. Buttons & Links
 * 11. Animations
 * 12. Responsive Overrides
 * ─────────────────────────────────────────────────────────────────
 */


/* ═══════════════════════════════════════════════════════════
   1. DESIGN TOKENS
   ═══════════════════════════════════════════════════════════ */
/* The tokens live in `static/tokens.css`, loaded before this file by every
   shell that uses it. They were here, and the Customer Portal shell and My
   Portal each carried a copy — three definitions of one palette, one of which
   disagreed. Do not restore a :root block here: a second definition wins or
   loses by load order, which is not a decision anybody makes on purpose. */


/* ═══════════════════════════════════════════════════════════
   2. GLOBAL UTILITIES
   ═══════════════════════════════════════════════════════════ */

/* Loading states */
.loading .table tbody       { opacity: .6; pointer-events: none; transition: opacity .2s; }
body.loading                { cursor: wait; }

/* Row highlight helpers */
.highlight                  { background-color: #ffebee !important; color: #d32f2f; }
.highlight.yellow           { background-color: #fff3e0 !important; }
.highlight.blue             { background-color: #e0f2f1 !important; }

/* Screen reader only */
.sr-only {
    position: absolute !important; width: 1px !important; height: 1px !important;
    padding: 0 !important; margin: -1px !important; overflow: hidden !important;
    clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important;
}


/* ═══════════════════════════════════════════════════════════
   3. REPORT TOOLBAR
   ═══════════════════════════════════════════════════════════ */
.report-toolbar {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .7rem 1rem;
    margin-bottom: .85rem;
    box-shadow: var(--shadow-xs);
}

.toolbar-row {
    display: flex;
    align-items: center;
    gap: 0;
    min-width: 0;
    width: 100%;
}

/* Left: FILTER label */
.toolbar-top-bar {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-shrink: 0;
    padding-right: .5rem;
    border-right: 1px solid var(--border);
    margin-right: .5rem;
}

.filter-icon-label {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    color: var(--text-muted);
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    flex-shrink: 0;
    margin-right: .4rem;
    white-space: nowrap;
}

/* Right: action icon buttons */
.toolbar-action-bar {
    padding-left: .6rem;
    border-left: 1px solid var(--border);
    margin-left: .4rem;
    display: flex;
    align-items: center;
    gap: .3rem;
    flex-shrink: 0;
}

/* Mobile: stack chips below label+actions */
@media (max-width: 767px) {
    .toolbar-row                { flex-wrap: wrap; align-items: center; }
    .toolbar-top-bar            { order: 1; flex: 0 0 auto; border-right: none; padding-right: 0; margin-right: 0; padding-bottom: .6rem; }
    .toolbar-action-bar         { order: 2; flex: 0 0 auto; margin-left: auto; border-left: none; padding-left: 0; padding-bottom: .6rem; }
    .chip-scroll-wrap           { order: 3; flex: 0 0 100%; width: 100%; padding-top: .55rem; border-top: 1px solid #dce5dc; }
    .chip-scroll-wrap .filter-chip-row { width: 100%; }
}


/* ── 3a. Filter Chips ─────────────────────────────────── */
.filter-chip-row {
    display: flex;
    align-items: center;
    gap: .28rem;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex: 1;
    min-width: 0;
    padding: 1px 0;
}
.filter-chip-row::-webkit-scrollbar { display: none; }

.chip-sep {
    width: 1px;
    height: 1.1rem;
    background: var(--border);
    flex-shrink: 0;
    margin: 0 .15rem;
    border-radius: 1px;
}

.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: .28rem;
    padding: .28rem .72rem;
    border-radius: 2rem;
    font-size: .775rem;
    font-weight: 500;
    border: 1.5px solid transparent;
    text-decoration: none;
    white-space: nowrap;
    transition: all .16s ease;
    line-height: 1;
    background: #eceff1;
    color: #546e7a;
    flex-shrink: 0;
}
.filter-chip:hover         { background: #dce5dc; text-decoration: none; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
.filter-chip.active        { background: #1b2e1c; color: #fff; border-color: #1b2e1c; box-shadow: 0 2px 6px rgba(17,24,39,.18); }
.filter-chip.active .chip-count { background: rgba(255,255,255,.18) !important; color: #fff !important; }

/* Color variants */
.filter-chip.chip-green        { background: #e8f5e9; color: #2e7d32; border-color: #a5d6a7; }
.filter-chip.chip-green:hover  { background: #c8e6c9; }
.filter-chip.chip-green.active { background: #43a047; color: #fff; border-color: #43a047; }

.filter-chip.chip-amber        { background: #fff3e0; color: #ef6c00; border-color: #ffcc80; }
.filter-chip.chip-amber:hover  { background: #ffe0b2; }
.filter-chip.chip-amber.active { background: #ef6c00; color: #fff; border-color: #ef6c00; }

.filter-chip.chip-red          { background: #ffebee; color: #d32f2f; border-color: #ef9a9a; }
.filter-chip.chip-red:hover    { background: #ffe4e6; }
.filter-chip.chip-red.active   { background: #d32f2f; color: #fff; border-color: #d32f2f; }

.filter-chip.chip-info         { background: #e0f2f1; color: #00897b; border-color: #80cbc4; }
.filter-chip.chip-info:hover   { background: #b2dfdb; }
.filter-chip.chip-info.active  { background: #00897b; color: #fff; border-color: #00897b; }

.filter-chip.chip-warning      { background: #fff3e0; color: #ef6c00; border-color: #ffcc80; }
.filter-chip.chip-warning:hover { background: #ffe0b2; }
.filter-chip.chip-warning.active { background: #ef6c00; color: #fff; border-color: #ef6c00; }

.filter-chip.chip-muted        { background: #eceff1; color: #546e7a; border-color: #dce5dc; }

/* EKM report chips. Values lifted verbatim from the two EKM templates, which
   carried their own copy of the whole chip row. chip-success matches chip-green
   exactly; chip-danger matches chip-red except for its hover, so they are kept
   separate rather than merged (see the register). */
.filter-chip.chip-success        { background: #e8f5e9; color: #2e7d32; border-color: #a5d6a7; }
.filter-chip.chip-success:hover  { background: #c8e6c9; }
.filter-chip.chip-success.active { background: #43a047; color: #fff; border-color: #43a047; }

.filter-chip.chip-danger         { background: #ffebee; color: #d32f2f; border-color: #ef9a9a; }
.filter-chip.chip-danger:hover   { background: #ffcdd2; }
.filter-chip.chip-danger.active  { background: #d32f2f; color: #fff; border-color: #d32f2f; }

.filter-chip.chip-secondary        { background: #f8fafc; color: #546e7a; border-color: #dce5dc; }
.filter-chip.chip-secondary:hover  { background: #eceff1; }
.filter-chip.chip-secondary.active { background: #546e7a; color: #fff; border-color: #546e7a; }

.filter-chip.chip-muted:hover  { background: #dce5dc; }
.filter-chip.chip-muted.active { background: #546e7a; color: #fff; border-color: #546e7a; }

.chip-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.2rem;
    height: 1.1rem;
    padding: 0 .28rem;
    border-radius: 1rem;
    font-size: .63rem;
    font-weight: 700;
    background: rgba(0,0,0,.1);
    color: inherit;
}


/* ── 3b. Chip scroll wrap + fade arrow ───────────────── */
.chip-scroll-wrap,
.scroll-x-wrap {
    position: relative;
    flex: 1 1 0%;
    min-width: 0;
}
.chip-scroll-wrap .filter-chip-row { flex: unset; width: 100%; }

/* Fade gradient on right edge */
.chip-scroll-wrap::after,
.scroll-x-wrap::after {
    content: '';
    position: absolute;
    top: 0; right: 0;
    width: 4.5rem; height: 100%;
    background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,.85) 50%, #fff 100%);
    pointer-events: none;
    transition: opacity .22s ease;
    z-index: 10;
}
/* Circular chevron on top of the fade */
.chip-scroll-wrap::before,
.scroll-x-wrap::before {
    content: '';
    position: absolute;
    top: 50%; right: .6rem;
    transform: translateY(-50%);
    width: 1.3rem; height: 1.3rem;
    background: #dce5dc url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23374151' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='9 18 15 12 9 6'/%3E%3C/svg%3E") center/60% no-repeat;
    border-radius: 50%;
    pointer-events: none;
    transition: opacity .22s ease;
    z-index: 11;
    box-shadow: 0 1px 3px rgba(0,0,0,.15);
}
/* Hide fade + arrow when scrolled to end */
.chip-scroll-wrap.at-end::after,
.chip-scroll-wrap.at-end::before,
.scroll-x-wrap.at-end::after,
.scroll-x-wrap.at-end::before { opacity: 0; pointer-events: none; }


/* ── 3c. Action Icon Buttons ──────────────────────────── */
.action-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: var(--radius-sm);
    background: #eceff1;
    color: var(--text-secondary);
    border: 1.5px solid var(--border);
    font-size: 1rem;
    cursor: pointer;
    text-decoration: none;
    transition: all .16s ease;
    flex-shrink: 0;
    padding: 0;
}
.action-icon-btn:hover      { background: #dce5dc; color: #546e7a; box-shadow: 0 2px 6px rgba(0,0,0,.08); }
button.action-icon-btn      { background: #eceff1; }

/* Larger variant for header areas */
.header-action-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    border-radius: var(--radius-sm);
    border: 1.5px solid var(--border);
    background: var(--surface);
    color: #546e7a;
    font-size: 1.1rem;
    cursor: pointer;
    transition: all .16s ease;
}
.header-action-icon:hover   { background: var(--surface-muted); box-shadow: var(--shadow-xs); }


/* ═══════════════════════════════════════════════════════════
   4. MOBILE CARDS  (report list pages)
   ═══════════════════════════════════════════════════════════ */
.mobile-card {
    background: var(--surface);
    border-bottom: 1px solid #dce5dc;
    cursor: pointer;
    transition: background .12s ease;
}
.mobile-card:hover { background: #f6f8f6; }

/* Status accent strips */
.mobile-card.card-danger  { border-left: 3px solid #d32f2f; background: #ffebee; }
.mobile-card.card-warning { border-left: 3px solid #ef6c00; background: #fff3e0; }
.mobile-card.card-muted   { border-left: 3px solid #546e7a; }
.mobile-card.card-success { border-left: 3px solid #43a047; }

/* Collapsed summary row.

   **This is where a mobile card's padding lives, not on `.mobile-card`.** The
   card itself carries only the background, the border and the accent strip, so
   a surface that writes its own row inside it loses the padding and the content
   sits against the card edge. Two feedback lists did exactly that. */
.card-summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .75rem 1rem;
}

/* A summary whose block runs to several lines -- a title, a chip row and a
   date. Centring a two-line control against a three-line block floats it in the
   middle of the card; this keeps it on the title, which is what a reader scans.
   The specificity is what makes it win: `.card-summary` sets `center` and
   `style.css` loads after the vendor utilities, so `.align-items-start` cannot. */
.card-summary.is-stacked { align-items: flex-start; }

.card-name-block { flex: 1; min-width: 0; }
.card-name {
    font-weight: 600;
    font-size: .88rem;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.card-meta {
    font-size: .72rem;
    color: var(--text-secondary);
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.card-badges {
    display: flex;
    align-items: center;
    gap: .3rem;
    flex-shrink: 0;
}
.card-expand-icon {
    color: var(--text-muted);
    font-size: .85rem;
    flex-shrink: 0;
    transition: transform .2s ease;
}
.mobile-card.is-open .card-expand-icon { transform: rotate(180deg); }

/* Expanded detail panel */
.card-detail {
    display: none;
    padding: .75rem 1rem .9rem 1rem;
    border-top: 2px solid #dce5dc;
    background: #f6f8f6;
    font-size: .8rem;
    box-shadow: inset 0 3px 8px rgba(0,0,0,.05);
}
.mobile-card.is-open .card-detail { display: block; }
.mobile-card.is-open {
    box-shadow: var(--shadow);
    z-index: 2;
    position: relative;
}

/* Detail rows */
.card-detail-row {
    display: flex;
    align-items: flex-start;
    gap: .4rem;
    padding: .35rem 0;
    border-bottom: 1px solid #f6f8f6;
    color: #546e7a;
}
.card-detail-row:last-child { border-bottom: none; }
.card-detail-label {
    font-size: .65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    min-width: 4.5rem;
    flex-shrink: 0;
    padding-top: 1px;
}
.card-detail-actions {
    display: flex;
    gap: .4rem;
    flex-wrap: wrap;
    padding-top: .6rem;
    margin-top: .25rem;
}


/* ═══════════════════════════════════════════════════════════
   5. CONTACT MOBILE CARDS  (account detail page)
   ═══════════════════════════════════════════════════════════ */
.contact-mobile-card {
    background: var(--surface);
    border-bottom: 1px solid #dce5dc;
    cursor: pointer;
    transition: background .12s ease;
}
.contact-mobile-card:hover { background: #f6f8f6; }
.contact-mobile-card .cmc-summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .75rem 1rem;
}
.contact-mobile-card .cmc-name {
    font-weight: 600;
    font-size: .875rem;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.contact-mobile-card .cmc-meta {
    font-size: .72rem;
    color: var(--text-secondary);
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.contact-mobile-card .cmc-expand-icon {
    color: var(--text-muted);
    font-size: .85rem;
    flex-shrink: 0;
    transition: transform .2s ease;
}
.contact-mobile-card.is-open .cmc-expand-icon { transform: rotate(180deg); }
.contact-mobile-card .cmc-detail {
    display: none;
    padding: .75rem 1rem .9rem 1rem;
    border-top: 2px solid #dce5dc;
    background: #f6f8f6;
    font-size: .8rem;
    box-shadow: inset 0 3px 8px rgba(0,0,0,.05);
}
.contact-mobile-card.is-open .cmc-detail { display: block; }
.contact-mobile-card.is-open {
    box-shadow: var(--shadow);
    z-index: 2;
    position: relative;
}
.cmc-detail-row {
    display: flex;
    align-items: flex-start;
    gap: .4rem;
    padding: .35rem 0;
    border-bottom: 1px solid #f6f8f6;
    color: #546e7a;
}
.cmc-detail-row:last-child { border-bottom: none; }
.cmc-detail-label {
    font-size: .65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    min-width: 4.5rem;
    flex-shrink: 0;
    padding-top: 1px;
}
.cmc-detail-actions {
    display: flex;
    gap: .4rem;
    flex-wrap: wrap;
    padding-top: .6rem;
    margin-top: .25rem;
}


/* ═══════════════════════════════════════════════════════════
   6. DATA TABLES
   ═══════════════════════════════════════════════════════════ */

/* Scroll wrapper */
.table-scroll-wrap,
.data-table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    position: relative;
}

/* Scrollbar styling */
.table-scroll-wrap::-webkit-scrollbar,
.data-table-wrap::-webkit-scrollbar { height: 6px; }
.table-scroll-wrap::-webkit-scrollbar-track,
.data-table-wrap::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
.table-scroll-wrap::-webkit-scrollbar-thumb,
.data-table-wrap::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
.table-scroll-wrap::-webkit-scrollbar-thumb:hover,
.data-table-wrap::-webkit-scrollbar-thumb:hover { background: var(--brand); }

/* Base table */
.data-table,
.table {
    border-collapse: separate;
    border-spacing: 0;
    width: 100%;
}

/* The navbar filter icon, green while filters are on. The panel collapses, so
   with it shut this is the only thing saying the report is narrowed. Set by
   `static/js/filter-indicator.js` from the active-filter row. */
#navbarFilterBtn.has-filters { color: var(--brand, #2E7C46); }

/* ── Active filter row ────────────────────────────────── */
/* One appearance for "what am I looking at", rendered by
   `includes/active_filters.html`. Values live here, never in a template. */
.active-filters {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    margin-bottom: .5rem;
}
.active-filters-label { color: var(--text-secondary); flex-shrink: 0; }
.active-filter-chip { font-weight: 500; }
/* Inherits the chip's text colour, so one rule serves every tone. */
.active-filter-remove {
    color: inherit;
    text-decoration: none;
    margin-left: .35rem;
    opacity: .6;
    transition: opacity .15s;
}
.active-filter-remove:hover,
.active-filter-remove:focus { color: inherit; opacity: 1; }
.active-filter-clear {
    font-size: .68rem;
    font-weight: 500;
    background: var(--bs-light, #f8f9fa);
    border: 1px solid var(--line, #dee2e6);
    color: var(--text-secondary) !important;
    text-decoration: none;
}
.active-filter-clear:hover { background: #eceff1; color: var(--text-primary) !important; }

/* ── A count pill sitting on a coloured results bar ───── */
/* `bg-white bg-opacity-25` cannot do this and must not be used for it. The
   vendor theme's `.bg-white` is `background-color: var(--bs-white)` -- a flat
   colour that never reads `--bs-bg-opacity` -- while `.bg-warning` and friends
   are `rgba(var(--bs-*-rgb), var(--bs-bg-opacity))` and do. So the opacity
   class silently did nothing and the pill rendered solid white with white text
   on it: a count nobody could read, on the one element whose whole job is to
   state a number. */
/* Darkened rather than lightened, and that is the whole point. A translucent
   WHITE pill carrying white text measures 3.05 against the grey bar and 2.38
   against the green one -- below AA even for large text, so the fix would have
   been a different unreadable count. Darkening instead gives 9.5 / 7.2 / 7.6
   across the three bars this badge sits on (secondary, success, danger), which
   clears AA for normal text on every one, and it still reads as a subtle pill
   rather than a second badge colour. */
.badge.count-on-bar {
    background-color: rgba(0, 0, 0, .38) !important;
    color: #fff !important;
}

/* ── 6a. Headers ──────────────────────────────────────── */
.data-table thead th,
.table thead th {
    background: #f6f8f6 !important;
    font-size: .68rem !important;
    font-weight: 700 !important;
    text-transform: uppercase !important;
    letter-spacing: .07em !important;
    color: var(--text-secondary) !important;
    border-bottom: 1px solid var(--border) !important;
    border-top: none;
    padding: .65rem .85rem !important;
    white-space: nowrap;
    position: sticky;
    top: 0;
    z-index: 1;
}

/* Resizer handle */
.data-table th.resizable,
.data-table thead th { position: relative; }
.data-table th .resizer {
    position: absolute; top: 0; right: -4px;
    width: 6px; height: 100%;
    cursor: col-resize;
    background: rgba(43,139,61,.1);
    transition: background .2s;
    z-index: 2;
    user-select: none;
}
.data-table th .resizer:hover { background: rgba(43,139,61,.4); }

/* Server-side sortable headers.
   The whole heading is the target, and the caret only darkens on the column
   actually in force -- an indicator on every column tells a reader nothing
   about which one is sorted. Distinct from `.sortable` below, which is the
   older client-side reorder and sorts only the rows on screen. */
.data-table thead th.sortable-col { padding-right: .5rem; }
.data-table thead th .sort-link {
    display: inline-flex;
    align-items: center;
    gap: .15rem;
    color: inherit;
    text-decoration: none;
    cursor: pointer;
}
.data-table thead th .sort-link:hover { color: var(--brand); }
.data-table thead th .sort-link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: 3px;
}
.data-table thead th .sort-caret { opacity: .25; font-size: 1rem; transition: opacity .15s; }
.data-table thead th .sort-link:hover .sort-caret { opacity: .6; }
.data-table thead th.is-sorted .sort-caret { opacity: 1; color: var(--brand); }
.data-table thead th.is-sorted { color: var(--brand); }

/* Sortable headers */
.data-table thead th.sortable {
    cursor: pointer;
    user-select: none;
    transition: background-color .2s;
}
.data-table thead th.sortable:hover   { background: var(--brand-light) !important; }
.data-table th .sort-indicator        { margin-left: 5px; font-size: .8em; opacity: .5; display: inline-block; transition: opacity .2s; }
.data-table th.sort-asc  .sort-indicator::after { content: "▲"; opacity: 1; color: var(--brand); }
.data-table th.sort-desc .sort-indicator::after { content: "▼"; opacity: 1; color: var(--brand); }

/* Sticky first column */
.data-table thead th:first-child,
.data-table tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--surface);
    box-shadow: 2px 0 6px -2px rgba(0,0,0,.08);
}
.data-table thead th:first-child { z-index: 3; background: #f6f8f6; }

/* ── 6b. Body cells & row tints ──────────────────────── */
.data-table tbody td {
    padding: .7rem .85rem !important;
    border-bottom: 1px solid #dce5dc;
    vertical-align: middle;
    font-size: .83rem;
}

/* Hover — blue tint on all cells including sticky column */
.data-table tbody tr:hover td,
.table-hover tbody tr:hover td     { background: var(--surface-hover) !important; transition: background .12s; }
.data-table tbody tr:hover td:first-child { background: var(--surface-hover) !important; }
.table tbody tr:focus               { outline: 2px solid var(--brand); outline-offset: -2px; }

/* Status tints */
.data-table tbody tr.row-danger td              { background: #ffebee; }
.data-table tbody tr.row-danger td:first-child  { background: #ffebee; border-left: 3px solid #d32f2f; }
.data-table tbody tr.row-warning td             { background: #fff3e0; }
.data-table tbody tr.row-warning td:first-child { background: #fff3e0; border-left: 3px solid #ef6c00; }
.data-table tbody tr.row-muted td               { background: #f6f8f6; }
.data-table tbody tr.row-muted td:first-child   { background: #f6f8f6; border-left: 3px solid #546e7a; }

/* acct-name / acct-sub pattern */
.acct-name {
    display: block;
    font-weight: 600;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 200px;
}
.acct-sub {
    display: block;
    font-size: .72rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 200px;
    margin-top: .1rem;
}


/* ═══════════════════════════════════════════════════════════
   7. CONFIRM MODAL
   ═══════════════════════════════════════════════════════════ */
.confirm-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 9999;
    background: rgba(15,23,42,.45);
    backdrop-filter: blur(3px);
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
}
.confirm-overlay.active { display: flex; }

.confirm-modal {
    background: var(--surface);
    border-radius: 1rem;
    box-shadow: 0 20px 60px rgba(0,0,0,.2), 0 4px 16px rgba(0,0,0,.12);
    width: 100%;
    max-width: 360px;
    overflow: hidden;
    animation: confirmIn .18s cubic-bezier(.34,1.56,.64,1);
}
@keyframes confirmIn {
    from { opacity: 0; transform: scale(.92) translateY(8px); }
    to   { opacity: 1; transform: scale(1) translateY(0); }
}

.confirm-modal-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3.2rem;
    height: 3.2rem;
    border-radius: 50%;
    font-size: 1.5rem;
    margin: 0 auto 1rem;
}
.confirm-modal-icon.icon-primary { background: #e0f2f1; color: #00897b; }
.confirm-modal-icon.icon-success { background: #e8f5e9; color: #43a047; }
.confirm-modal-icon.icon-danger  { background: #ffebee; color: #d32f2f; }
.confirm-modal-icon.icon-warning { background: #fff3e0; color: #ef6c00; }

.confirm-modal-body   { padding: 1.75rem 1.5rem 1.25rem; text-align: center; }
.confirm-modal-title  { font-size: 1rem; font-weight: 700; color: var(--text-primary); margin-bottom: .4rem; }
.confirm-modal-msg    { font-size: .85rem; color: var(--text-secondary); line-height: 1.5; margin: 0; }
.confirm-modal-footer { display: flex; gap: .6rem; padding: 1rem 1.5rem 1.5rem; }
.confirm-modal-footer .btn { flex: 1; border-radius: .6rem; font-weight: 600; font-size: .875rem; padding: .5rem; }


/* ═══════════════════════════════════════════════════════════
   8. CARDS & SNAPSHOTS  (account detail)
   ═══════════════════════════════════════════════════════════ */
.modern-card {
    border-radius: var(--radius) !important;
    overflow: hidden;
    box-shadow: var(--shadow-sm) !important;
    transition: box-shadow .25s, transform .25s;
}
.modern-card:hover {
    box-shadow: var(--shadow) !important;
    transform: translateY(-2px);
}

/* One radius for a card and everything inside its edge. The vendor theme rounds
   a card at its own size; a header rounded at ours left the card's corner
   showing outside the header's. The header reads the card's inner radius, so
   the two cannot drift apart again. No calc against the border width: the
   theme sets it to a unitless 0, which makes any calc on it invalid. */
.card {
    --bs-card-border-radius: var(--radius);
    --bs-card-inner-border-radius: var(--radius);
}
.card-header {
    background-color: var(--bg-light);
    border-bottom: 1px solid var(--bg-lighter) !important;
    padding: 1rem 1.25rem !important;
    border-radius: var(--bs-card-inner-border-radius, var(--radius)) var(--bs-card-inner-border-radius, var(--radius)) 0 0 !important;
}
.card-header h6 { color: var(--text-color) !important; font-weight: 600; }

/* Snapshot cards */
.snapshot-card {
    border-radius: var(--radius) !important;
    box-shadow: var(--shadow-xs) !important;
    background: var(--surface);
    transition: all .25s;
    position: relative;
    overflow: hidden;
}
.snapshot-card::before {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    border-radius: var(--radius) var(--radius) 0 0;
}
.snapshot-card:hover { box-shadow: var(--shadow) !important; transform: translateY(-1px); }

.snapshot-icon {
    width: 32px; height: 32px;
    border-radius: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: .5rem;
    font-size: 1rem;
}
.snapshot-label {
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-secondary);
    font-weight: 600;
    margin-bottom: .25rem;
}
.snapshot-value {
    font-size: 1.4rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--text-primary);
}
.snapshot-sub { font-size: .75rem; margin-top: .15rem; }
.detail-page .snapshot-value, .detail-page .snapshot-sub { font-variant-numeric: tabular-nums; }
.detail-page .snapshot-card.accent-brand::before { background: var(--brand); }
.detail-page .snapshot-card.accent-teal::before  { background: var(--teal); }
.detail-page .snapshot-card.accent-slate::before { background: var(--slate); }
.detail-page .snapshot-card.accent-amber::before { background: var(--amber); }

/* ==========================================================================
   DIVISION BADGES
   One badge per division on the wallet-share card: filled where the customer
   already buys that division, dashed where they do not (the cross-sell target).
   The meaning is also in each badge's tooltip, never in colour alone.
   ========================================================================== */
.div-check {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.4rem; height: 1.4rem; border-radius: .3rem; font-size: .65rem; font-weight: 700;
}
.div-check.service-has   { background: var(--brand-light); color: var(--brand-dark); }
.div-check.marketing-has { background: var(--amber-light); color: var(--amber); }
.div-check.voip-has      { background: var(--teal-light);  color: var(--teal); }
.div-check.it-has        { background: var(--slate-light); color: var(--slate); }
.div-check.miss-it       { background: var(--surface-muted); border: 1.5px dashed var(--border); color: var(--text-secondary); }
/* How many of the four divisions a customer buys: one is an island, three
   or more integrated. The figure carries it; the colour reinforces. */
.dcb-island     { color: var(--amber); }
.dcb-growing    { color: var(--text-secondary); }
.dcb-integrated { color: var(--brand); }

/* ==========================================================================
   DETAIL HEADER
   The record page's header card (ui-canon section 1): icon tile, the record's
   name, its badges, actions on the right, an info strip beneath. The detail
   pages each still carry a page-local copy of these rules; this is the one
   definition a page uses instead of adding another. Every rule is scoped to
   `.detail-page`, which a page opts into on its outer wrapper, so the pages
   still on their own copies are untouched until each moves.
   ========================================================================== */
.detail-page .hdr-row { display: flex; align-items: flex-start; gap: .85rem; min-width: 0; }
.detail-page .hdr-tile {
    width: 44px; height: 44px; flex-shrink: 0;
    border-radius: 10px; background: var(--brand); color: var(--text-on-brand);
    display: inline-flex; align-items: center; justify-content: center; font-size: 1.35rem;
}
.detail-page .hdr-name-block { flex: 1; min-width: 0; }
.detail-page .hdr-name {
    font-size: 1.15rem; font-weight: 700; line-height: 1.25; margin: 0;
    color: var(--text-primary); text-wrap: balance;
}
.detail-page .hdr-sub { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .5rem; margin-top: .3rem;
           font-size: .8rem; color: var(--text-secondary); }
.detail-page .hdr-actions { display: flex; align-items: center; gap: .35rem; flex-shrink: 0; }
.detail-page .hdr-info-strip {
    display: flex; flex-wrap: wrap; gap: .5rem .9rem;
    font-size: .775rem; color: var(--text-secondary);
    padding-top: .6rem; margin-top: .75rem;
    border-top: 1px solid var(--border);
}
.detail-page .hdr-info-strip a { color: var(--brand); text-decoration: none; }
@media (max-width: 575.98px) {
    .detail-page .hdr-tile { width: 36px; height: 36px; border-radius: 9px; font-size: 1.1rem; }
    .detail-page .hdr-name { font-size: 1rem; }
}

/* The period a record page is read over, inside its header card and always
   visible. A date that steers the whole page is not a filter to hide behind
   the navbar funnel. One slim row under a hairline: no panel of its own. */
.detail-page .period-bar {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .6rem;
    margin-top: .85rem; padding-top: .75rem; border-top: 1px solid var(--border);
}
.detail-page .period-bar .pb-field { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.detail-page .period-bar .pb-label { font-size: .65rem; font-weight: 600; letter-spacing: .04em;
                        text-transform: uppercase; color: var(--text-secondary); }
.detail-page .period-bar .form-control, .detail-page .period-bar .form-select {
    font-size: .82rem; height: 36px; padding-top: .25rem; padding-bottom: .25rem;
    font-variant-numeric: tabular-nums;
}
.detail-page .period-bar input[type=date] { width: 9.75rem; }
.detail-page .period-bar .pb-step {
    width: 36px; height: 36px; padding: 0; display: inline-flex; align-items: center;
    justify-content: center;
    transition-property: transform, background-color; transition-duration: .12s;
    transition-timing-function: ease-out;
}
.detail-page .period-bar .pb-step:active:not(.disabled), .detail-page .period-bar .pb-apply:active { transform: scale(0.96); }
.detail-page .period-bar .pb-step.disabled { opacity: .45; pointer-events: none; }
.detail-page .period-bar .pb-apply { height: 36px; padding: 0 .9rem; transition: transform .12s ease-out; }
.detail-page .period-bar .pb-tech { flex: 0 1 17rem; }
.detail-page .period-bar .pb-gap { width: .4rem; }
@media (max-width: 575.98px) {
    .detail-page .period-bar input[type=date] { width: 100%; }
    .detail-page .period-bar .pb-field { flex: 1 1 7.5rem; }
    .detail-page .period-bar .pb-tech { flex: 1 1 100%; }
    .detail-page .period-bar .pb-gap { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .detail-page .period-bar .pb-step, .detail-page .period-bar .pb-apply { transition: none; }
}

/* Explainer tooltip trigger. Sits beside a section title; used by eight
   templates and previously had no definition at all, so it rendered as an
   unspaced glyph in body colour. */
.info-tip {
    margin-left: .35rem;
    font-size: .85rem;
    line-height: 1;
    color: var(--text-secondary);
    cursor: help;
    vertical-align: middle;
}
.info-tip:hover { color: var(--brand); }

/* Section icon pill */
.section-icon {
    width: 28px; height: 28px;
    border-radius: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .85rem;
    margin-right: .5rem;
}


/* ═══════════════════════════════════════════════════════════
   9. HEALTH CIRCLE
   ═══════════════════════════════════════════════════════════ */
.health-circle {
    position: relative;
    width: 80px; height: 80px;
    border-radius: 50%;
    background: conic-gradient(var(--brand) calc(var(--score) * 1%), #f1f3f4 0);
    display: flex;
    align-items: center;
    justify-content: center;
}
.health-circle::before {
    content: '';
    width: 60px; height: 60px;
    border-radius: 50%;
    background: var(--surface);
    position: absolute;
    box-shadow: 0 2px 8px rgba(0,0,0,.1);
}
.health-score-text {
    position: relative;
    z-index: 1;
    font-weight: bold;
    font-size: 1.1rem;
    color: var(--brand) !important;
}


/* ═══════════════════════════════════════════════════════════
   10. BUTTONS & LINKS
   ═══════════════════════════════════════════════════════════ */
.action-btn {
    transition: all .2s;
    border-radius: 6px !important;
    font-weight: 500;
}
.action-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,.1); }
.action-btn.btn-primary:hover,
.action-btn.btn-success:hover {
    background-color: var(--brand-dark) !important;
    border-color: var(--brand-dark) !important;
}
.btn:disabled { opacity: .65; cursor: not-allowed; }

.quick-link-btn {
    border-radius: var(--radius-sm) !important;
    font-size: .8rem;
    font-weight: 500;
    padding: .4rem .85rem;
    transition: all .2s;
    border-width: 1.5px !important;
}
.quick-link-btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-xs); }

/* Modern dropdown */
.modern-dropdown {
    border-radius: var(--radius);
    box-shadow: 0 4px 16px rgba(0,0,0,.12);
    border: none !important;
    min-width: 180px;
    padding: .5rem 0;
}
.modern-dropdown .dropdown-item {
    padding: .5rem 1rem;
    transition: all .2s;
    border: none;
    background: none;
    width: 100%;
    text-align: left;
    display: flex;
    align-items: center;
    color: var(--text-color);
    text-decoration: none;
}
.modern-dropdown .dropdown-item:hover,
.modern-dropdown .dropdown-item:focus  { background: var(--brand-light) !important; color: var(--brand) !important; }
.modern-dropdown .dropdown-item.active { background: var(--brand) !important; color: #fff !important; }

/* Green-tinted table hover (used globally) */
.table-hover tbody tr:hover { background-color: var(--brand-extra-light) !important; transition: background-color .15s; }


/* ═══════════════════════════════════════════════════════════
   11. ANIMATIONS
   ═══════════════════════════════════════════════════════════ */
.ajax-generated {
    animation: slideInFromTop .3s ease-out;
}
@keyframes slideInFromTop {
    from { opacity: 0; transform: translateY(-20px); }
    to   { opacity: 1; transform: translateY(0); }
}

.btn .bx-loader-alt { animation: spin 1s linear infinite; }
@keyframes spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

.summary-footer .card-body {
    background: linear-gradient(135deg, var(--bg-light) 0%, var(--bg-lighter) 100%) !important;
    border-radius: 0 0 var(--radius) var(--radius);
}

/* Dropdown chevron rotation (integration accordion) */
.integration-chevron { transition: transform .3s ease; }


/* ═══════════════════════════════════════════════════════════
   12. RESPONSIVE OVERRIDES
   ═══════════════════════════════════════════════════════════ */

/* Large: hide resizer handles */
@media (max-width: 1199.98px) {
    .data-table th .resizer { display: none; }
    .data-table thead th.sortable { padding-right: 1.5rem; }
}

/* Medium */
@media (max-width: 768px) {
    .health-circle        { width: 70px; height: 70px; }
    .health-circle::before { width: 52px; height: 52px; }
    .health-score-text    { font-size: 1rem; }
    .modern-card          { border-radius: 8px !important; }

    .metric-card .card-body,
    .system-card .card-body,
    .contact-card .card-body { padding: 1rem !important; }

    .system-card .card-header { padding: .75rem 1rem !important; font-size: .9rem; }

    /* Header card on account detail */
    .header-card .card-body   { padding: 1rem !important; }
    .account-detail-container h1.h4 { font-size: 1.1rem !important; }
    .quick-link-btn           { font-size: .75rem !important; padding: .3rem .6rem !important; }
}

/* Small */
@media (max-width: 576px) {
    .account-detail-container { padding-left: .75rem !important; padding-right: .75rem !important; }

    .health-circle        { width: 60px; height: 60px; }
    .health-circle::before { width: 44px; height: 44px; }
    .health-score-text    { font-size: .9rem; }
    .modern-card          { border-radius: 6px !important; }

    /* Snapshot grid on small screens */
    .snapshot-value       { font-size: 1.15rem !important; }
    .snapshot-label       { font-size: .62rem !important; }
    .snapshot-card .card-body { padding: .65rem !important; }

    .status-badge         { font-size: .7rem !important; padding: .375rem .5rem; }
    .system-card .card-header { padding: .625rem .875rem !important; font-size: .8rem; }
    .system-card .card-body   { padding: .875rem !important; }
    .btn-sm               { font-size: .7rem; padding: .25rem .5rem; }
}

/* Prevent hover lift on dashboard header card */
.dash_hdr.card:hover { transform: none !important; border: none !important; }

/* ── Collapsed menu: clip hidden link areas so they can't be clicked ──────
   In icon-only mode (5.25rem) the full-width <a> elements still extend
   beyond the visible sidebar. overflow:hidden clips both rendering and
   pointer-events to the sidebar's actual width.
   Re-enable on hover so the expanded fly-out panel is still usable.        */
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu {
    overflow: hidden;
}
.layout-menu-collapsed.layout-menu-hover #layout-menu {
    overflow: visible;
}


/* ==========================================================================
   FEEDBACK DETAIL DIALOG  (.fb-dialog)

   The shared shell for a dialog in the feedback module. The request page's
   Change status dialog uses it and any later one must too: two copies of a
   dialog drift the moment one is adjusted, which is what happened to the filter
   panel (register U-201).

   It deliberately matches the submission widget (feedback_widget.html): same
   radius, border, shadow, header rhythm, uppercase field labels, muted footer
   and pill buttons, so the module reads as one thing whichever dialog opens.
   The widget's tokens are scoped to `.fb-widget` and cannot be reached from
   here, so the values are restated on `.fb-dialog`; lifting the widget onto
   these is its own change, not this one's.

   The shell only. A surface keeps its own content classes -- a comment thread,
   a vote control -- and this owns header, body rhythm and footer.
   ========================================================================== */

.fb-dialog {
    --fbd-brand:       #43a047;
    --fbd-brand-light: #e8f5e9;
    --fbd-surface:     #ffffff;
    --fbd-muted:       #f6f8f6;
    --fbd-border:      #dce5dc;
    --fbd-text:        #1b2e1c;
    --fbd-text-sec:    #5f7461;
    --fbd-radius:      12px;
    --fbd-radius-sm:   8px;
}

.fb-dialog .modal-content   { border-radius: var(--fbd-radius); border: 1px solid var(--fbd-border);
                              box-shadow: 0 8px 32px rgba(0, 0, 0, .13); overflow: hidden; }

.fb-dialog .modal-header    { display: flex; align-items: flex-start; gap: .75rem;
                              background: var(--fbd-surface);
                              border-bottom: 1px solid var(--fbd-border);
                              padding: 1rem 1.25rem .85rem; }
.fb-dialog .modal-header .btn-close { position: static; transform: none; margin: 0 0 0 auto;
                              padding: .25rem; opacity: .65; background-color: transparent;
                              box-shadow: none; border-radius: .25rem; flex-shrink: 0; }
.fb-dialog .modal-header .btn-close:hover { opacity: 1; background-color: var(--fbd-muted); }

/* Title row: a soft icon chip, the title, then a one-line explainer under it --
   the widget's shape, so the two dialogs open the same way. */
.fb-dialog-title            { font-size: .93rem; font-weight: 700; color: var(--fbd-text);
                              display: flex; align-items: center; gap: .45rem;
                              margin: 0 0 .2rem; line-height: 1.3; }
.fb-dialog-icon             { width: 1.7rem; height: 1.7rem; border-radius: .4rem;
                              background: var(--fbd-brand-light); color: var(--fbd-brand);
                              display: inline-flex; align-items: center; justify-content: center;
                              font-size: .85rem; flex-shrink: 0; }
.fb-dialog-sub              { font-size: .7rem; color: var(--fbd-text-sec);
                              line-height: 1.45; margin: 0; }

.fb-dialog .modal-body      { padding: 1rem 1.25rem; }
.fb-dialog-label            { font-size: .62rem; font-weight: 700; text-transform: uppercase;
                              letter-spacing: .07em; color: var(--fbd-text-sec);
                              margin-bottom: .4rem; display: block; }

/* Pills carry what they are, because a row of identical grey chips leaves a
   reader guessing which one is the status. */
.fb-dialog-head             { min-width: 0; }
.fb-dialog-label-spaced     { margin-top: 1rem; }
.fb-dialog-label-flush      { margin-bottom: 0; }
.fb-dialog-badges           { display: flex; flex-wrap: wrap; gap: .3rem; }
.fb-dialog-inline-actions   { display: flex; align-items: center; flex-wrap: wrap;
                              gap: .5rem; margin: 1rem 0; }
.fb-dialog-pill             { display: inline-flex; align-items: center; gap: .3rem;
                              padding: .28rem .7rem; border-radius: 2rem;
                              border: 1px solid var(--fbd-border); background: var(--fbd-surface);
                              font-size: .7rem; font-weight: 600; color: var(--fbd-text); }
.fb-dialog-pill-k           { font-size: .56rem; font-weight: 700; letter-spacing: .05em;
                              text-transform: uppercase; color: var(--fbd-text-sec); }

.fb-dialog-input            { font-size: .82rem; border-radius: var(--fbd-radius-sm);
                              border-color: var(--fbd-border); }
.fb-dialog-input:focus      { border-color: var(--fbd-brand);
                              box-shadow: 0 0 0 .15rem rgba(67, 160, 71, .15); }
.fb-dialog-message          { font-size: .82rem; line-height: 1.55; color: var(--fbd-text);
                              white-space: pre-wrap; padding: .85rem .95rem;
                              background: var(--fbd-muted); border: 1px solid var(--fbd-border);
                              border-radius: var(--fbd-radius-sm); }
.fb-dialog-error            { font-size: .72rem; color: #d32f2f; display: none; }
.fb-dialog-hidden           { display: none; }
.fb-dialog-note             { font-size: .68rem; color: var(--fbd-text-sec);
                              font-style: italic; margin: .5rem 0 0; }

/* Radio choices, the widget's pill selector. The radio stays in the markup for
   the form and assistive technology; its label is the control. A final choice
   (closing, say) sits after the separator in the widget's amber, so ending a
   request never looks like one more step along it. */
.fb-dialog-choices          { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem; }
.fb-dialog-choices input[type="radio"] { position: absolute; opacity: 0; width: 0; height: 0; }
.fb-dialog-choices label    { display: inline-flex; align-items: center; gap: .28rem; margin: 0;
                              padding: .28rem .7rem; border-radius: 2rem; line-height: 1.3;
                              border: 1.5px solid var(--fbd-border); background: var(--fbd-surface);
                              font-size: .73rem; font-weight: 600; color: var(--fbd-text-sec);
                              white-space: nowrap; cursor: pointer; user-select: none;
                              transition: all .15s; }
.fb-dialog-choices label:hover { border-color: var(--fbd-brand); color: var(--fbd-brand);
                              background: var(--fbd-brand-light); }
.fb-dialog-choices label.is-final { --fbd-chip: #ef6c00; color: #ef6c00; }
.fb-dialog-choices label.is-final:hover { border-color: #ef6c00; color: #ef6c00;
                              background: #fff3e0; }
.fb-dialog-choices input[type="radio"]:checked + label { color: #fff;
                              background: var(--fbd-chip, var(--fbd-brand));
                              border-color: var(--fbd-chip, var(--fbd-brand)); }
.fb-dialog-choices input[type="radio"]:focus-visible + label { outline: 2px solid var(--fbd-brand);
                              outline-offset: 2px; }
.fb-dialog-choice-k         { font-size: .6rem; font-weight: 600; opacity: .75; }
.fb-dialog-choices-sep      { width: 1px; align-self: stretch; margin: 0 .3rem;
                              background: var(--fbd-border); }
.fb-dialog-req              { color: #d32f2f; margin-left: .1rem; }
.fb-dialog legend.fb-dialog-label { float: none; width: auto; }

/* The outcome panel echoes the widget's "About this page" strip. */
.fb-dialog-where            { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
                              margin-top: 1rem; padding: .55rem .8rem;
                              background: var(--fbd-muted); border: 1px solid var(--fbd-border);
                              border-radius: var(--fbd-radius-sm); font-size: .75rem; }
.fb-dialog-where:first-child { margin-top: 0; }
.fb-dialog-where a          { color: var(--fbd-brand); text-decoration: none; font-weight: 600; }
.fb-dialog-where a:hover    { text-decoration: underline; }

.fb-dialog .modal-footer    { background: var(--fbd-muted); border-top: 1px solid var(--fbd-border);
                              padding: .8rem 1.25rem; }
.fb-dialog-actions          { display: flex; flex-wrap: wrap; align-items: center;
                              justify-content: flex-end; gap: .5rem; width: 100%; }
.fb-dialog-actions .btn     { display: inline-flex; align-items: center; gap: .35rem;
                              border-radius: 2rem; padding: .4rem 1rem;
                              font-size: .75rem; font-weight: 600; }
.fb-dialog-actions .btn-dark,
.fb-dialog-actions .btn-primary { background: var(--fbd-brand); border-color: var(--fbd-brand);
                              color: #fff; }
.fb-dialog-actions .btn-dark:hover,
.fb-dialog-actions .btn-primary:hover { background: #2e7d32; border-color: #2e7d32; }


/* ==========================================================================
   SEARCHABLE SELECT  (.ss-*)
   Paired with static/js/searchable-select.js. A picker that stays usable when
   the list is long: a filter box, a match count, keyboard selection.

   It is layered over a native <select>, which stays in the form and remains
   what submits. With no JavaScript the native control shows through and the
   page still works, which is why `.ss-button` and `.ss-panel` only exist once
   the script has upgraded the field.

   No library was added for this. Nothing suitable is vendored, and a dependency
   needs approval first (Rule 8).
   ========================================================================== */

.ss-wrap                { position: relative; }

/* The native control is the value; it is hidden only once JS has replaced it. */
.ss-wrap .ss-native     { position: absolute; opacity: 0; pointer-events: none;
                          width: 100%; height: 0; }

.ss-button              { text-align: left; cursor: pointer; white-space: nowrap;
                          overflow: hidden; text-overflow: ellipsis; }

.ss-panel               { display: none; position: absolute; z-index: 1060;
                          top: calc(100% + 4px); left: 0; right: 0;
                          background: #fff; border: 1px solid var(--border, #dce5dc);
                          border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.12);
                          overflow: hidden; }
.ss-wrap.is-open .ss-panel { display: block; }

.ss-search              { display: flex; align-items: center; gap: .45rem;
                          padding: .5rem .65rem; border-bottom: 1px solid var(--border, #dce5dc); }
.ss-search i            { font-size: .95rem; color: #5f7461; flex-shrink: 0; }
.ss-search input        { flex: 1; border: none; outline: none; font-size: .8rem;
                          background: transparent; color: #1b2e1c; }

.ss-count               { padding: .3rem .65rem; font-size: .62rem; font-weight: 700;
                          letter-spacing: .05em; text-transform: uppercase;
                          color: #5f7461; background: #f6f8f6; }

.ss-list                { list-style: none; margin: 0; padding: .25rem;
                          max-height: 260px; overflow-y: auto; }
.ss-option              { padding: .45rem .6rem; border-radius: 6px; cursor: pointer;
                          font-size: .8rem; color: #1b2e1c; }
.ss-option:hover,
.ss-option.is-active    { background: #e8f5e9; }
.ss-option.is-selected  { font-weight: 600; }
.ss-option.is-selected::after { content: 'selected'; float: right; font-size: .6rem;
                          text-transform: uppercase; letter-spacing: .05em;
                          color: #43a047; font-weight: 700; }
.ss-empty               { padding: .75rem .6rem; font-size: .78rem; color: #5f7461; }
/* ===========================================================
 * 12. Permission matrix, split pair, admin feed
 * The matrix is rendered on /profile and /users/<id>/profile through
 * includes/permission_matrix.html. **The values live here, not in
 * either template**: while they sat in profile.html the admin page
 * carried its own `.perm-badge` and `.perm-cat-count` with different
 * values, so one class name painted two different components.
 *
 * The card's own rules are scoped under `.perm-matrix`, its root
 * class. Role Manager independently uses `.perm-search` and
 * `.perm-toolbar` for a different component, and unscoped these
 * reached it -- a shared component may not claim a bare class name.
 * =========================================================== */
  .perm-matrix .perm-badge {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    font-size: 0.78rem;
    line-height: 1.35;
    padding: 0.16rem 0.1rem;
    color: var(--bs-body-color);
    cursor: default;
    break-inside: avoid;
  }
  .perm-matrix .perm-badge:hover { color: var(--bs-primary); }
  /* Colour by what the permission lets somebody do. The word is always beside
     it: colour alone carries nothing to a screen reader or a printed page. */
  .perm-matrix .perm-badge.kind-elevated { color: var(--bs-danger); }
  .perm-matrix .perm-badge.kind-write    { color: var(--bs-warning-text-emphasis, #997404); }
  .perm-matrix .perm-badge.kind-filter   { color: var(--bs-info-text-emphasis, #087990); }
  .perm-matrix .perm-badge i { font-size: 0.7rem; flex: none; color: var(--bs-secondary-color); }
  .perm-matrix .perm-badge.kind-elevated i { color: var(--bs-danger); }
  .perm-matrix .perm-badge.kind-write i    { color: var(--bs-warning); }
  .perm-matrix .perm-badge.kind-filter i   { color: var(--bs-info); }
  .perm-matrix .perm-kind-tag {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
  }
  .perm-matrix .kind-elevated .perm-kind-tag { color: var(--bs-danger); }
  .perm-matrix .kind-write .perm-kind-tag    { color: var(--bs-warning-text-emphasis, #997404); }
  .perm-matrix .perm-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
    margin-top: 0.9rem;
  }
  .perm-matrix .perm-legend span { display: inline-flex; align-items: center; gap: 0.3rem; }
  .perm-matrix .perm-legend i { font-size: 0.7rem; }

  /* One row per area, closed until asked for. 180 permissions rendered at once
     buried the rest of the page, and nobody reads a wall of chips. */
  .perm-matrix .perm-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0 0 1.25rem;
  }
  .perm-matrix .perm-search { max-width: 20rem; flex: 1 1 14rem; }
  .perm-matrix .perm-summary-chip {
    font-size: 0.72rem;
    font-weight: 600;
    padding: 0.3rem 0.65rem;
    border-radius: 1rem;
    background: rgba(var(--bs-secondary-rgb), 0.08);
    border: 1px solid rgba(var(--bs-secondary-rgb), 0.15);
    color: var(--bs-secondary-color);
    white-space: nowrap;
  }
  .perm-matrix .perm-summary-chip.elevated {
    background: rgba(var(--bs-danger-rgb), 0.07);
    border-color: rgba(var(--bs-danger-rgb), 0.2);
    color: var(--bs-danger);
  }

  .perm-matrix .perm-groups {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0 2rem;
    align-items: start;
  }
 @media (min-width: 1600px) {
    .perm-matrix .perm-groups { grid-template-columns: 1fr 1fr; }
  }
  .perm-matrix .perm-group { border-top: 1px solid var(--bs-border-color); }
  .perm-matrix .perm-group[open] { grid-column: 1 / -1; }
  .perm-matrix .perm-groups .perm-group:last-child { border-bottom: 1px solid var(--bs-border-color); }
  .perm-matrix .perm-group > summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0.25rem;
    cursor: pointer;
    list-style: none;
    font-size: 0.85rem;
  }
  .perm-matrix .perm-group > summary::-webkit-details-marker { display: none; }
  .perm-matrix .perm-group > summary:hover { background: rgba(var(--bs-primary-rgb), 0.03); }
  .perm-matrix .perm-group > summary:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: -2px; }
  .perm-matrix .perm-group-chevron { transition: transform 0.15s; color: var(--bs-secondary-color); }
  .perm-matrix .perm-group[open] > summary .perm-group-chevron { transform: rotate(90deg); }
  .perm-matrix .perm-group-name { font-weight: 600; }
  .perm-matrix .perm-group-meta {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
  }
  .perm-matrix .perm-cat-count {
    font-size: 0.68rem;
    padding: 0.1rem 0.45rem;
    border-radius: 1rem;
    background: rgba(var(--bs-primary-rgb), 0.1);
    color: var(--bs-primary);
    font-weight: 600;
  }
  .perm-matrix .perm-group > .perm-badges-wrap { display: none; }
  .perm-matrix .perm-group[open] > .perm-badges-wrap {
    display: block;
    columns: 12rem;
    column-gap: 1.5rem;
    padding: 0.15rem 0.25rem 0.9rem 1.6rem;
  }
  .perm-matrix .perm-filter-note {
    font-size: 0.78rem;
    color: var(--bs-secondary-color);
    margin: 1.5rem 0 0.5rem;
    padding-top: 0.25rem;
  }
    /* Side by side, the pair reads as one row only if both stop at the same
     height, so each scrolls inside its own card rather than setting the row's. */
  .split-pair > [class*="col-"] > .card { max-height: 39rem; }
  .split-body { display: flex; flex-direction: column; min-height: 0; }
  .card .card-body.split-body { padding-top: 1.25rem; }
  .card .card-body.split-body.p-0 { padding-top: 0; }
  .side-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 0.35rem; }
  .sticky-head th { position: sticky; top: 0; z-index: 1; }
  .perm-detail-cell { max-width: 18rem; overflow-wrap: anywhere; }

  /* A four column table does not survive half a page, so the history reads as a
     feed: what happened, to whom, when, and the detail underneath. */
  .admin-feed { list-style: none; margin: 0; padding: 0; }
  .admin-feed-item {
    display: flex;
    gap: 0.75rem;
    padding: 0.8rem 1.25rem;
    border-bottom: 1px solid var(--bs-border-color);
  }
  .admin-feed-item:last-child { border-bottom: 0; }
  .admin-feed-icon {
    flex: none;
    width: 2rem;
    height: 2rem;
    border-radius: 0.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.95rem;
  }
  .admin-feed-body { min-width: 0; flex: 1; }
  .admin-feed-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    justify-content: space-between;
  }
  .admin-feed-action { font-size: 0.85rem; font-weight: 600; }
  .admin-feed-when {
    flex: none;
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
    font-variant-numeric: tabular-nums;
  }
  .admin-feed-who { font-size: 0.8rem; display: flex; flex-wrap: wrap; gap: 0.4rem; }
  .admin-feed-who a { color: var(--bs-body-color); font-weight: 500; text-decoration: none; }
  .admin-feed-who a:hover { text-decoration: underline; }
  .admin-feed-detail {
    font-size: 0.78rem;
    color: var(--bs-secondary-color);
    margin-top: 0.2rem;
    overflow-wrap: anywhere;
  }
  .perm-matrix .perm-no-match { display: none; font-size: 0.85rem; color: var(--bs-secondary-color); padding: 0.9rem 0.25rem; }

/* ==== PAGE ALERT BAR ====
   A full-width bar above the navbar saying the whole page is in an unusual
   state, with one action on the right. Two of them exist and they are the same
   component: impersonation and the overnight data pause.

   **Above the search, not under it.** Under the navbar it read as a property of
   the page you were looking at; the state it describes belongs to the whole
   session, so it sits where the impersonation bar already sat and pushes the
   chrome down.

   The tone carries the meaning and the words repeat it, because colour alone
   is not a signal (`Docs/ui-canon.md`): danger is a state you are acting
   inside, notice is a temporary state you should know about. */
.page-alert-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: .5rem;
    flex-shrink: 0;
    padding: .5rem 1.5rem;
    font-size: .8125rem;
    font-weight: 500;
    line-height: 1.5;
    color: #fff;
}
.page-alert-bar__text { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.page-alert-bar i { flex: none; font-size: 1rem; }

/* Impersonation: you are acting as somebody else and every action lands as
   them. The strongest colour the palette has, because the consequence is. */
.page-alert-bar--danger { background: var(--red); }

/* The overnight pause: a temporary state with an action, not a fault. Amber
   rather than the brand green, which would read as "all good", and rather than
   the impersonation red, which would read as "something is wrong". */
.page-alert-bar--notice { background: var(--amber); }

.page-alert-bar__action {
    font: inherit;
    font-weight: 600;
    white-space: nowrap;
    padding: .2rem .8rem;
    border-radius: .375rem;
    cursor: pointer;
    color: #fff;
    background: rgba(255, 255, 255, .2);
    border: 1px solid rgba(255, 255, 255, .5);
}
.page-alert-bar__action:hover { background: rgba(255, 255, 255, .3); }
.page-alert-bar__action:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* The cooldown sentence sits where the button would be, so the row does not
   reflow when the action becomes available again. */
.page-alert-bar__note { white-space: nowrap; opacity: .9; }

@media (max-width: 767.98px) {
    .page-alert-bar { padding: .5rem .75rem; align-items: flex-start; }
}


/* ============================================================================
   THE FILTER CARD -- one definition, every report
   ----------------------------------------------------------------------------
   This lived in thirteen template <style> blocks and had already drifted: AR
   sized its inputs at 90px and invoice at 110px, `.nf-help` was styled on one
   report so the info icon beside a label was unstyled on the other twelve, and
   five reports never styled `input[type=number]` at all, so their amount boxes
   wore the browser default. One component, one place (ui-canon 2).

   **A grid, not a wrapping flex row.** The row laid fields out with mixed
   flex bases, so a report with ten filters wrapped wherever the widths happened
   to run out and left one field stranded on a second line with the buttons
   beside it. Equal columns that reflow at any field count cannot do that, and
   the actions take a row of their own so they are never mid-line.

   `.nf-sm`, `.nf-md` and `.nf-date` survive as no-ops on the input width: the
   grid decides column width now, and keeping the class names means no template
   had to change to get this layout.
   ========================================================================== */

.navbar-filter-panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 .375rem .375rem;
    padding: .7rem 1.5rem .75rem;
    box-shadow: 0 2px 8px rgba(0, 0, 0, .06);
    overflow: visible;
    opacity: 1;
    /* The panel hangs off the navbar above it and matches its inset, which is
       why it is sized rather than left to fill: eleven templates carried this
       geometry and two had lost it, so those two panels ran full width and sat
       a couple of pixels adrift of the bar they belong to. */
    inline-size: calc(100% - 1.625rem * 2);
    margin-inline: auto;
    margin-top: -2px;
}
.navbar-filter-panel.collapsed { display: none; }

.navbar-filter-form {
    display: grid;
    /* One track sizing rule for every report. `auto-fit` collapses the empty
       tracks so a four-filter card fills the width instead of leaving six
       ghost columns, and the min stops a date input from being squeezed
       under its own control. */
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: .6rem .65rem;
    align-items: end;
}

.nf-field {
    display: flex;
    flex-direction: column;
    gap: .18rem;
    min-width: 0;
}

.nf-label {
    font-size: .58rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--slate);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Every control the card can hold, including the number and search inputs
   several reports never styled. */
.nf-field input,
.nf-field select,
.nf-field textarea {
    width: 100%;
    min-width: 0;
    font-size: .78rem;
    padding: .3rem .5rem;
    border: 1.5px solid var(--border);
    border-radius: .4rem;
    color: var(--slate);
    background: var(--surface);
    transition: border-color .16s ease, box-shadow .16s ease;
}
.nf-field input:focus,
.nf-field select:focus,
.nf-field textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-light);
}
.nf-field input:disabled,
.nf-field select:disabled {
    background: var(--surface-muted);
    color: var(--text-muted, #90a4ae);
    cursor: not-allowed;
}

/* The width classes are inert now: the grid owns column width, and a fixed
   input width inside a grid cell only reintroduces the ragged row. */
.nf-sm input, .nf-sm select,
.nf-md input, .nf-md select,
.nf-date input, .nf-date select { width: 100%; }

.nf-help {
    font-size: .8rem;
    color: var(--text-muted, #90a4ae);
    cursor: help;
    vertical-align: -1px;
}

/* The one reusable categorical multi-select for the navbar filter row
   (Docs/ui-canon.md, "The navbar filter row"): a native <details> styled to
   read exactly like the `.nf-field select` beside it when closed, so it never
   lifts off the row's shared baseline the way a `multiple` or `size` select
   would. Open, the checklist is an absolutely positioned panel -- it never
   reflows the grid the other filters sit in. */
.nf-ms { position: relative; }
.nf-ms-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .35rem;
    width: 100%;
    font-size: .78rem;
    padding: .3rem .5rem;
    border: 1.5px solid var(--border);
    border-radius: .4rem;
    color: var(--slate);
    background: var(--surface);
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.nf-ms-summary::-webkit-details-marker { display: none; }
.nf-ms-summary:focus-visible {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-light);
}
.nf-ms[open] > .nf-ms-summary { border-color: var(--brand); }
.nf-ms-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nf-ms-caret { font-size: .85rem; color: var(--text-muted, #90a4ae); flex-shrink: 0; }
.nf-ms[open] .nf-ms-caret { transform: rotate(180deg); }

.nf-ms-panel {
    position: absolute;
    z-index: 20;
    top: calc(100% + .25rem);
    left: 0;
    /* `width: auto` on an absolutely positioned box already shrinks to fit
       its content, bounded below and above by min/max-width -- not
       `min-width: 100%` (matched a wide "Division"-style grid column, which
       is what pushed every row toward the right edge) and not
       `width: max-content` paired with 100%-width children below (the two
       size each other in a circle that some browsers resolve by collapsing
       the panel to the checkbox alone, forcing the label off-screen with a
       horizontal scrollbar). */
    width: auto;
    min-width: 9rem;
    max-width: 16rem;
    max-height: 14rem;
    overflow-y: auto;
    overflow-x: hidden;
    background: var(--surface);
    border: 1.5px solid var(--border);
    border-radius: .4rem;
    box-shadow: 0 6px 18px rgba(0, 0, 0, .1);
    padding: .3rem;
}
.nf-ms-option {
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: center;
    gap: .4rem;
    font-size: .8rem;
    color: var(--slate);
    text-align: left;
    padding: .3rem .4rem;
    border-radius: .3rem;
    cursor: pointer;
    white-space: nowrap;
}
.nf-ms-option:hover { background: var(--surface-muted); }
.nf-ms-option input[type="checkbox"] {
    /* `.nf-field input { width: 100% }` above reaches this checkbox too --
       it is a descendant of the same `.nf-field` wrapper -- and without an
       explicit override here it wins on specificity, stretching the
       checkbox across the whole row and pushing the label off the edge of
       the panel. */
    width: auto;
    margin: 0;
    cursor: pointer;
    flex-shrink: 0;
    order: -1;
}
.nf-ms-option .nf-ms-option-label { text-align: left; }

/* **The actions own a row.** Spanning every column is what stops Clear and
   Apply from being dealt into the field flow and stranded beside whichever
   filter happened to wrap last. */
.nf-actions {
    grid-column: 1 / -1;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: .4rem;
    margin-top: .1rem;
    padding-top: .55rem;
    border-top: 1px solid var(--border);
}

.nf-apply-btn {
    font-size: .75rem !important;
    font-weight: 600 !important;
    border-radius: .4rem !important;
    padding: .3rem .85rem !important;
    white-space: nowrap;
    height: auto !important;
}
.nf-clear-btn {
    font-size: .75rem;
    font-weight: 500;
    border-radius: .4rem;
    padding: .3rem .75rem;
    white-space: nowrap;
    color: var(--text-secondary);
    background: var(--slate-light);
    border: 1.5px solid var(--border);
    cursor: pointer;
    transition: all .16s ease;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    text-decoration: none;
    line-height: 1.4;
}
.nf-clear-btn:hover {
    background: #ffcdd2;
    color: var(--red);
    border-color: #ef9a9a;
    text-decoration: none;
}

@media (max-width: 767px) {
    .navbar-filter-panel { padding: .6rem .75rem .7rem; }
    /* Two columns on a phone rather than one: a date range reads as a pair,
       and a single column turns a ten-filter card into a page of scrolling. */
    .navbar-filter-form { grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); }
}


/* ==========================================================================
   SETTINGS SURFACES
   The look of an editable `app_settings` row, and the page furniture around
   one. TWO pages render these: Settings, and the Notifications administration
   page where each notification carries its own settings behind a disclosure.
   Both include `includes/setting_row.html`, so both need this vocabulary and
   neither may hold a copy of it (Rule 14c: the canon owns the part, style.css
   owns the values).
   ========================================================================== */
/* Page-scoped tokens. Every value comes from the shared palette in style.css,
   so this surface tracks the theme instead of carrying its own colours. */
.settings-surface {
    --set-rail      : var(--brand);
    --set-rail-soft : var(--brand-light);
    --set-line      : var(--border);
    --set-panel     : var(--surface);
    --set-panel-alt : var(--surface-muted);
    --set-ink       : var(--text-primary);
    --set-ink-soft  : var(--text-secondary);
    --set-ink-mute  : var(--text-muted);
}

/* -- Sidebar ------------------------------------------------------------ */
.settings-sidebar { position: sticky; top: 1.25rem; }
.settings-nav-card {
    border: 1px solid var(--set-line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--set-panel);
}
.nav-section-label {
    font-size: .6875rem; font-weight: 700; letter-spacing: .07em;
    text-transform: uppercase; color: var(--set-ink-mute);
    padding: .65rem .85rem .25rem;
}
.settings-nav .nav-link {
    display: flex; align-items: center; gap: .5rem;
    color: var(--set-ink-soft);
    border-radius: var(--radius-sm);
    padding: .45rem .75rem; margin: .1rem .4rem;
    font-size: .9rem; font-weight: 500; white-space: nowrap;
    transition: background .12s, color .12s;
}
.settings-nav .nav-link:hover  { background: var(--set-panel-alt); color: var(--set-ink); }
.settings-nav .nav-link.active { background: var(--set-rail-soft); color: var(--brand-dark); font-weight: 600; }
.settings-nav .nav-link i { font-size: 1rem; flex-shrink: 0; width: 18px; text-align: center; }
.nav-divider { margin: .4rem .85rem; border-top: 1px solid var(--set-line); }

.info-card {
    border: 1px solid var(--set-line);
    border-radius: var(--radius);
    background: var(--set-panel);
    padding: .9rem 1rem;
    font-size: .8125rem; color: var(--set-ink-soft); line-height: 1.5;
}

/* -- Content card -------------------------------------------------------- */
.content-card {
    border: 1px solid var(--set-line);
    border-radius: var(--radius);
    background: var(--set-panel);
    overflow: hidden;
}

/* -- Category tabs ------------------------------------------------------- */
.cat-tabs {
    display: flex; flex-wrap: wrap; gap: .15rem;
    border-bottom: 1px solid var(--set-line);
    background: var(--set-panel-alt);
    padding: 0 1rem;
}
.cat-tab-btn {
    border: none; background: none;
    padding: .65rem .9rem .55rem;
    font-size: .875rem; font-weight: 500; white-space: nowrap;
    color: var(--set-ink-soft);
    border-bottom: 2px solid transparent;
    cursor: pointer; transition: color .12s, border-color .12s;
}
.cat-tab-btn:hover  { color: var(--set-ink); }
.cat-tab-btn.active { color: var(--brand-dark); border-bottom-color: var(--set-rail); font-weight: 600; }
.count-pill {
    display: inline-block; margin-left: .35rem; vertical-align: middle;
    font-size: .7rem; font-weight: 600;
    background: var(--set-panel); color: var(--set-ink-mute);
    border: 1px solid var(--set-line); border-radius: 20px;
    padding: .02rem .4rem;
}
.cat-tab-btn.active .count-pill { background: var(--set-rail-soft); color: var(--brand-dark); border-color: transparent; }

/* -- Toolbar ------------------------------------------------------------ */
.set-toolbar {
    display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
    padding: .75rem 1.1rem;
    border-bottom: 1px solid var(--set-line);
}
.set-search-wrap { position: relative; flex: 1 1 260px; max-width: 380px; }
.set-search-wrap i {
    position: absolute; left: .65rem; top: 50%; transform: translateY(-50%);
    font-size: 1rem; color: var(--set-ink-mute); pointer-events: none;
}
.set-search {
    width: 100%; padding: .4rem .7rem .4rem 2rem;
    font-size: .875rem;
    border: 1px solid var(--set-line); border-radius: var(--radius-sm);
    background: var(--set-panel); color: var(--set-ink);
}
.set-search:focus {
    outline: none; border-color: var(--set-rail);
    box-shadow: 0 0 0 .175rem var(--set-rail-soft);
}
.set-count { font-size: .8125rem; color: var(--set-ink-mute); }

/* -- Capability blocks --------------------------------------------------- */
.cat-panel { display: none; }
.cat-panel.active { display: block; }

/* A search answers for the whole page, not the open tab: a setting the reader
   remembers is often filed under the other one. Every category shows while a
   term is present, each under its own name. */
.is-searching .cat-panel { display: block; }
.panel-label {
    display: none;
    padding: .5rem 1.1rem;
    font-size: .6875rem; font-weight: 700; letter-spacing: .07em;
    text-transform: uppercase; color: var(--set-ink-mute);
    background: var(--set-panel-alt);
    border-bottom: 1px solid var(--set-line);
}
.is-searching .panel-label { display: block; }
.is-searching .cat-panel.has-no-match { display: none; }

/* Sub-grouping within one panel (Notifications, spanning several domains).
   Always visible, unlike .panel-label above -- there is no tab name to lean
   on here, so the group name is the only thing that says what follows. */
.panel-sublabel {
    padding: .5rem 1.1rem .375rem;
    font-size: .6875rem; font-weight: 700; letter-spacing: .07em;
    text-transform: uppercase; color: var(--set-ink-mute);
}
.set-subgroup + .set-subgroup { border-top: 1px solid var(--set-line); }
.set-subgroup.has-no-match { display: none; }

.set-block { border-bottom: 1px solid var(--set-line); }
.set-block:last-child { border-bottom: none; }

.set-row {
    display: flex; align-items: flex-start; gap: 1.25rem;
    padding: .75rem 1.1rem;
    transition: background .12s;
}
.set-row:hover { background: var(--set-panel-alt); }
.set-row + .set-row { border-top: 1px solid var(--set-line); }
.set-row.changed { background: var(--set-rail-soft); box-shadow: inset 3px 0 0 var(--set-rail); }

/* The switch row leads its block and carries the capability name. */
.set-row.is-switch .set-label { font-size: .9375rem; font-weight: 600; }
.set-row.is-switch { background: var(--set-panel-alt); }
.set-row.is-switch:hover { background: var(--surface-hover); }

/* Children sit inside one continuous rail, so a recipient field reads as part
   of the capability above it rather than as a setting of its own. */
.set-children {
    margin-left: 1.1rem;
    border-left: 2px solid var(--set-rail);
    border-top: 1px solid var(--set-line);
}

/* A capability and its settings: the switch row always on screen, everything it
   governs behind the disclosure beside it. Thirty-seven settings read as
   thirteen rows, and thirty more notifications add thirty rows rather than a
   hundred fields. */
.set-block-head { display: flex; align-items: center; gap: .35rem; padding-left: .35rem; }
.set-block-head > .set-row { flex: 1 1 auto; min-width: 0; }
.set-block.is-open { background: var(--set-panel-alt); }
.set-block.is-open > .set-block-head > .set-row { background: transparent; }

.set-main { flex: 1 1 auto; min-width: 0; }
.set-label { font-size: .875rem; font-weight: 600; color: var(--set-ink); }
.set-help  { font-size: .8125rem; color: var(--set-ink-mute); margin-top: .1rem; line-height: 1.45; }

.set-control { flex: 0 0 auto; display: flex; align-items: center; gap: .6rem; padding-top: .1rem; }
.set-control .form-control,
.set-control .form-select { width: 15rem; max-width: 42vw; font-size: .875rem; }
.set-control .form-control:focus,
.set-control .form-select:focus {
    border-color: var(--set-rail); box-shadow: 0 0 0 .175rem var(--set-rail-soft);
}
.set-control .form-check-input:checked { background-color: var(--set-rail); border-color: var(--set-rail); }
.set-control .form-check-input:focus { border-color: var(--set-rail); box-shadow: 0 0 0 .175rem var(--set-rail-soft); }

/* A switch row kept on screen only to name the block beneath it. */
.set-row.is-context { opacity: .62; }
.set-row.is-context .set-control { pointer-events: none; }

/* The state is a word, never the switch colour alone. */
.state-word { font-size: .8125rem; font-weight: 600; min-width: 2.1rem; color: var(--set-ink-soft); }
.state-word.is-on { color: var(--brand-dark); }

.btn-reset {
    border: none; background: none; padding: 0;
    font-size: .75rem; font-weight: 600; color: var(--brand-dark);
    text-decoration: underline; cursor: pointer;
}
.btn-reset:hover { color: var(--set-ink); }

/* -- Notices and states -------------------------------------------------- */
.set-hidden-count {
    border-left: 1px solid var(--set-line);
    padding-left: .75rem;
    cursor: help;
}

.empty-state { padding: 2.75rem 1.5rem; text-align: center; color: var(--set-ink-mute); }
.empty-state i { font-size: 2rem; display: block; margin-bottom: .5rem; }
.empty-state p { font-size: .9375rem; font-weight: 600; color: var(--set-ink-soft); margin: 0 0 .25rem; }
.empty-state small { font-size: .8125rem; }

/* The connection report reuses the settings row, so a read-only line and an
   editable one sit on one grid. Its heading is always shown, unlike the
   category label above, which only appears while a search is running. */
.panel-label.is-static { display: block; }

/* A readiness word. **Severity is never carried by colour alone** (ui-canon
   14c): the word IS the state, and the tint only reinforces it, so a reader
   who cannot separate the hues loses nothing. */
.status-word { min-width: 6.5rem; text-align: right; display: inline-block; }
.status-word.is-ready     { color: var(--brand-dark); }
.status-word.is-blocked   { color: var(--red); }
.status-word.is-incomplete,
.status-word.is-not-configured { color: var(--set-ink-soft); }
.status-word.is-disabled  { color: var(--set-ink-mute); }
/* The summary panel collapses "not configured" and "incomplete" into one word,
   so it needs the same muted treatment under its own name. */
.status-word.is-not-ready { color: var(--set-ink-soft); }

/* -- A disclosure inside a settings card -------------------------------- */
.set-details > summary {
    padding: .55rem 1rem; cursor: pointer;
    font-size: .82rem; color: var(--set-ink-soft);
    border-top: 1px solid var(--border);
    list-style: none;
}
.set-details > summary::-webkit-details-marker { display: none; }
.set-details > summary::before { content: "\25B8"; margin-right: .45rem; }
.set-details[open] > summary::before { content: "\25BE"; }
.set-details > summary:hover { color: var(--set-ink); }
.set-details > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* -- CONNECTION CARD (ui-canon 2j) ---------------------------------------
   One outside connection on one row: name, environment and connection status
   on the left; its actions and its Enabled switch on the right. The row wraps,
   so a phone stacks the actions under the name instead of scrolling sideways.
   The switch keeps a `.set-row[data-key]` of its own for setting-inputs.js,
   flattened here so it reads as a control rather than a nested row. */
.conn-card {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: .75rem 1.25rem; padding: .85rem 1.1rem;
}
.conn-card-main { flex: 1 1 18rem; min-width: 0; }
.conn-card-title { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
    font-size: .9375rem; font-weight: 600; color: var(--set-ink); }
.conn-card-status, .conn-card-facts { margin-top: .25rem; font-size: .8125rem; color: var(--set-ink-soft); }
.conn-card-facts { color: var(--set-ink-mute); }
.conn-card-status .status-word { min-width: 0; text-align: left; margin-right: .4rem; }
.conn-card-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: .5rem; }
.conn-card-actions form { margin: 0; }
.conn-card-actions .set-row.conn-card-switch { padding: .2rem .35rem; gap: .5rem; align-items: center;
    background: transparent; border-radius: var(--radius); }
.conn-card-actions .set-row.conn-card-switch.changed { background: var(--set-rail-soft); box-shadow: none; }
.conn-card-confirm > summary { list-style: none; }
.conn-card-confirm > summary::-webkit-details-marker { display: none; }
/* Open, the confirmation takes a line of its own at the end of the actions. */
.conn-card-confirm[open] { order: 10; flex-basis: 100%; }
.conn-card-form { flex: 1 1 100%; padding-top: .75rem; border-top: 1px solid var(--set-line); }
.conn-token-form { display: flex; flex-direction: column; align-items: stretch; gap: .25rem; width: 100%;
    max-width: 36rem; text-align: left; }
.conn-token-row { display: flex; gap: .5rem; align-items: center; }
.conn-token-row .form-control { flex: 1 1 auto; min-width: 0; }
.conn-token-row .btn { flex: none; white-space: nowrap; }
.conn-token-help { margin: .2rem 0 0; font-size: .78rem; color: var(--set-ink-mute); }
.conn-card-confirm-panel .conn-token-form { margin-left: auto; }
.conn-card-confirm-panel { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
    gap: .5rem .75rem; margin-top: .5rem; font-size: .8125rem; color: var(--set-ink-soft); text-align: right; }
/* The switch is a control in this row, not a settings row, so the settings
   rows' phone rule that stacks a row into a column does not apply to it. */
@media (max-width: 767.98px) {
    .conn-card-actions .set-row.conn-card-switch { flex-direction: row; }
}
@media (max-width: 575.98px) {
    .conn-card-actions { justify-content: flex-start; }
    .conn-card-confirm-panel { justify-content: flex-start; text-align: left; }
}

/* -- Save bar ------------------------------------------------------------ */
.save-bar {
    position: fixed; left: 50%; bottom: 1.25rem; transform: translate(-50%, 200%);
    z-index: 1035;
    display: flex; align-items: center; gap: 1rem;
    padding: .6rem .75rem .6rem 1.1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 999px;
    box-shadow: var(--shadow);
    transition: transform .18s ease-out;
}
.save-bar.is-visible { transform: translate(-50%, 0); }
.save-bar-text { font-size: .875rem; font-weight: 600; color: var(--text-primary); white-space: nowrap; }

/* -- Page header --------------------------------------------------------- */
.page-icon-wrap {
    width: 2.4rem; height: 2.4rem; border-radius: var(--radius-sm);
    background: var(--set-rail-soft);
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.page-icon-wrap i { font-size: 1.2rem; color: var(--brand-dark); }

.toast-success { border-left: 3px solid var(--brand); }
.toast-error   { border-left: 3px solid var(--red); }

@media (max-width: 767.98px) {
    .set-row { flex-direction: column; gap: .5rem; }
    .set-control .form-control,
    .set-control .form-select { width: 100%; max-width: none; }
}

/* -- Notification list ------------------------------------------------------
   The administration page: a category heading, then one row per notification
   with its own settings behind a disclosure. Uses the `.set-row` vocabulary
   above for the settings themselves, so an expanded notification looks exactly
   like the Settings page and there is one thing to keep in step. */

/* The disclosure, and there is one of it. Both settings surfaces open a section
   this way, so a second class for the same control would be two rules and two
   selectors drifting apart (UI canon 2h). */
.disclose {
    flex: 0 0 auto; width: 1.9rem; height: 1.9rem; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: none; border: 1px solid transparent; border-radius: 6px;
    color: var(--set-ink-mute); cursor: pointer;
}
.disclose:hover { background: var(--set-panel-alt); color: var(--set-ink); }
.disclose:focus-visible {
    outline: none; border-color: var(--set-rail);
    box-shadow: 0 0 0 .175rem var(--set-rail-soft);
}
.disclose.is-empty { cursor: default; }
.disclose-chev { font-size: 1.15rem; transition: transform .15s ease; }
.disclose[aria-expanded="true"] .disclose-chev { transform: rotate(90deg); }

/* How many settings a closed section holds. A collapsed row that does not say
   what is inside makes a reader open every one to find out. */
.disclose-count {
    flex: 0 0 auto; min-width: 1.35rem; text-align: center;
    font-size: .6875rem; font-weight: 600; line-height: 1.35rem;
    color: var(--set-ink-mute); background: var(--set-panel-alt);
    border: 1px solid var(--set-line); border-radius: 999px;
}




@media (max-width: 575.98px) {
    .set-block-head { align-items: flex-start; }
    .set-block-head > .set-row { flex-wrap: wrap; }
    .set-children .set-row { flex-wrap: wrap; }
    .set-children .set-control { width: 100%; padding-top: .5rem; }
    .set-children .set-control .form-control,
    .set-children .set-control .form-select { width: 100%; max-width: none; }
}

/* -- Module access matrix (Customer Portal settings page) ------------------
   Contact type x module, same `--set-*` tokens as the rest of this surface.
   A grid rather than `.set-row` rows because the shape is genuinely a table,
   not a list of scalar settings. */
.module-access-table { width: 100%; font-size: .875rem; margin: 0; }
.module-access-table th {
    font-size: .75rem; font-weight: 700; color: var(--set-ink-mute);
    padding: .55rem .85rem; text-align: center; white-space: nowrap;
    border-bottom: 1px solid var(--set-line);
}
.module-access-table th:first-child { text-align: left; }
.module-access-table td {
    padding: .55rem .85rem; border-top: 1px solid var(--set-line);
    vertical-align: middle; text-align: center;
}
.module-access-table tbody tr:hover { background: var(--set-panel-alt); }

/* ==== EMAIL TEMPLATE LIST ====
   The same table, carrying names and sentences rather than a grid of
   checkboxes. Centring is right for the grid and wrong for prose, so text
   reads from the left here and only the action column stays right. Qualified
   by the id, or the centring rules above win on specificity. */
#templateTable th, #templateTable td { text-align: left; }
#templateTable th.tpl-action, #templateTable td.tpl-action { text-align: right; }
/* Qualified by the table, or `.module-access-table td` above wins on
   specificity and centres the contact type under a left-aligned head. */
.module-access-table td.module-access-type {
    font-weight: 600; color: var(--set-ink); text-align: left;
}
.module-access-table input[type="checkbox"] { width: 1.05rem; height: 1.05rem; cursor: pointer; }
.module-access-table input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .55; }

/* ==========================================================================
   MY NOTIFICATIONS
   The card on /profile: one person's email preferences. Deliberately NOT the
   settings vocabulary above -- this card holds no `app_settings` row, and
   borrowing `.set-row` would make a personal preference look like a
   configuration screen and invite an administrative control onto it.

   Tiles in one packed grid, matching the tiles the profile page already uses
   for Feedback: a full-width row per notification would push the permissions
   card below the fold, which is what moving this off its own page was meant to
   avoid.
   ========================================================================== */
.mynotif-sendto {
    display: flex; align-items: center; gap: .45rem;
    font-size: .8125rem; color: var(--text-secondary);
    margin: 0 0 .9rem .15rem;
}
.mynotif-sendto i { font-size: .95rem; opacity: .8; }

.mynotif-failed {
    display: flex; align-items: flex-start;
    font-size: .8125rem; color: var(--red);
    border: 1px solid var(--red); border-radius: 8px;
    background: var(--red-light);
    padding: .6rem .75rem; margin: 0;
}

/* One grid for every notification, packed. A grid per category left short
   categories sitting beside empty tracks, so the card was mostly gap; the
   category moved onto the tile and the tracks fill. */
.mynotif-grid {
    display: grid; gap: .7rem; margin: 0;
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}

.mynotif-tile {
    display: flex; flex-direction: column;
    border: 1px solid var(--border); border-radius: 8px;
    padding: .7rem .8rem;
    transition: background .12s;
}
/* **A tile that is on is not filled**, so there is no `is-on` rule to find
   here. Every tile on the profile page is a plain bordered panel, and tinting
   the followed half made the card read as a different surface bolted onto the
   page. The state is carried by the word `On`/`Off` and by its one button,
   which is what the reader acts on. */
.mynotif-tile:hover { background: var(--surface-muted); }

/* The category, on the tile rather than as a heading over a part-empty row.
   It is the quietest thing on the tile: the name is what a reader scans for. */
.mynotif-cat {
    font-size: .65rem; font-weight: 700; letter-spacing: .07em;
    text-transform: uppercase; color: var(--text-secondary);
    margin-bottom: .15rem;
}
.mynotif-name { font-size: .9375rem; font-weight: 600; color: var(--text-primary); line-height: 1.3; }
.mynotif-desc {
    font-size: .8125rem; color: var(--text-secondary);
    margin-top: .15rem; line-height: 1.45;
}
.mynotif-sev { font-size: .6875rem; margin-right: auto; }

/* The state and its one button sit on the tile's own line, pushed to the
   bottom so tiles of differing description length still end level. */
.mynotif-foot {
    display: flex; align-items: center; gap: .55rem;
    margin-top: auto; padding-top: .6rem;
}
.mynotif-state { font-size: .8125rem; font-weight: 600; }
.mynotif-state.on  { color: var(--brand-dark); }
.mynotif-state.off { color: var(--text-secondary); }

@media (max-width: 575.98px) {
    .mynotif-grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   SUMMARY BAR
   One band of figures between the filters and a list: the label above, the
   value under it, a thin rule between cells and a brand accent down the left.
   The AR aging report set this appearance and three report templates hold
   their own inline copy of it; this is the shared one, and the register tracks
   moving those onto it.

   It states figures and never filters. The chip row above owns the filtering,
   so nothing in here is a link.
   ========================================================================== */
.summary-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 4px solid var(--brand);
    border-radius: .75rem;
    padding: .55rem .35rem;
    margin-bottom: .85rem;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .05);
}
/* The cells share the width rather than bunching at the left. AR's bar fills
   because it carries ten of them; this one carries three, and left-aligned they
   read as a band that failed to load the rest. */
.summary-bar-cell {
    flex: 1 1 0;
    min-width: 7rem;
    padding: .3rem 1.15rem;
    border-radius: var(--radius-sm, 6px);
    text-decoration: none;
    color: inherit;
    transition: background .15s;
}
.summary-bar-cell:hover { background: var(--surface-muted); text-decoration: none; }
.summary-bar-cell:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}
/* The step being shown. A background alone would be a colour carrying the
   message, so the label goes dark and heavy with it. */
.summary-bar-cell.is-selected { background: var(--surface-muted); }
.summary-bar-cell.is-selected .summary-bar-label { color: var(--text-primary); }

/* The steps run in order, so what sits between them points. It is a direction,
   not a separator, which is why it replaced the rule that used to be here. */
.summary-bar-arrow {
    align-self: center;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background: var(--surface-muted);
    border: 1px solid var(--border);
    color: var(--text-secondary);
}
.summary-bar-arrow i { font-size: .95rem; line-height: 1; }
.summary-bar-label {
    display: block;
    margin-bottom: .3rem;
    font-size: .62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--text-secondary);
    white-space: nowrap;
}
.summary-bar-value {
    display: block;
    font-size: 1.35rem;
    font-weight: 600;
    line-height: 1.1;
    color: var(--text-primary);
}
/* Tone reinforces the chip of the same name. The label beside it already says
   which step this is, so colour is never carrying the meaning on its own. */
.summary-bar-value.tone-amber { color: var(--amber); }
.summary-bar-value.tone-info  { color: var(--teal); }

@media (max-width: 575.98px) {
    .summary-bar-cell { flex: 1 1 auto; min-width: 0; padding: .3rem .55rem; }
    .summary-bar-arrow { width: 1.2rem; height: 1.2rem; }
    .summary-bar-arrow i { font-size: .8rem; }
}

/* ==========================================================================
   DASHBOARD SHELL
   The parts a population dashboard is built from (Docs/ui-canon.md 1a): one
   compact header (identity left, controls right, a rule beneath), quiet
   maintenance actions, the card every section sits in, and the grid of two
   parents with a full-width breakout. The Account dashboard is the reference
   and the Service Calls dashboard uses the same parts; each page keeps its
   own token block, which these rules read.
   ========================================================================== */
/* -- Page header ------------------------------------------- */
/* One header rather than three controls that happen to sit in a row: the
   identity on the left, the controls right-aligned and ordered by how often
   they are used, separated from the page by a rule rather than by guesswork. */
.dash-header {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: 1rem 1.5rem; flex-wrap: wrap;
    padding-bottom: .85rem; margin-bottom: 1.25rem;
    border-bottom: 1px solid var(--border);
}
.dash-header-id { min-width: 0; }
.dash-page-title {
    margin: 0; font-size: 1.15rem; font-weight: 700; line-height: 1.2;
    color: var(--text-primary); letter-spacing: -.01em;
}
.dash-header-meta {
    display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
    margin-top: .3rem; font-size: .72rem; color: var(--text-secondary);
}
.dash-chip-quiet {
    padding: .1rem .45rem; border-radius: 999px; font-size: .62rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: .05em;
    background: var(--surface-muted); border: 1px solid var(--border);
    color: var(--text-secondary);
}
.dash-header-controls {
    display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
}
/* Maintenance, not a page action: it reads as a link until you reach for it. */
.dash-action-quiet {
    display: inline-flex; align-items: center; gap: .2rem;
    font-size: .72rem; color: var(--text-secondary); text-decoration: none;
    padding: .25rem .4rem; border-radius: var(--radius-sm);
}
.dash-action-quiet:hover { color: var(--brand); background: var(--surface-muted); }
.dash-action-label { margin-left: .25rem; }
@media (max-width: 575px) {
    .dash-action-label { display: none; }
}
/* The one page action shares the family's corner, so the three controls do not
   each arrive with their own idea of a button. */
.dash-header-controls .dash-action { border-radius: var(--radius-sm); }
.dash-card {
    background: var(--surface); border: 1px solid var(--border) !important;
    border-radius: var(--radius) !important; box-shadow: var(--shadow-sm) !important;
    transition: box-shadow .2s, transform .2s;
}
.dash-card:hover { box-shadow: var(--shadow) !important; transform: translateY(-1px); }
/* -- The shell: two parents and one breakout ----------------
   One grid. Markup order is reading order, and it is the only order below
   `xl`; from there the two parents sit side by side and the portfolio spans
   both. The right parent runs its own grid because what it holds is smaller
   and pairs naturally, which is also what keeps the two parents finishing near
   each other without a fixed height anywhere. */
.dash-shell { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.dash-parent-left, .dash-parent-right {
    display: grid; gap: 1rem; align-content: start; min-width: 0;
    /* An explicit track: an implicit one sizes to its widest child's
       min-content, so one unwrappable header pushed a whole column past a
       phone's edge. */
    grid-template-columns: minmax(0, 1fr);
}
.dash-right-cell { min-width: 0; }
/* The summary toolbar itself: one bar under the header. A page-scoped copy
   still overrides this on the pages that carry one (register U-397). */
.summary-toolbar {
    background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--brand);
    border-radius: var(--radius); padding: .75rem 1.25rem; box-shadow: var(--shadow-sm);
}
.toolbar-label { font-size: .65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--text-secondary); }
.summary-toolbar .stat-value { display: block; font-size: 1.3rem; font-weight: 700; line-height: 1.15; color: var(--text-primary); }
.summary-toolbar .stat-sub { font-size: .7rem; color: var(--text-secondary); }
/* Ordered bands (aging, age): one linked row per band, figure and share on
   the right and a bar beneath, in band order, which a donut would lose. */
.band-row {
    display: grid; grid-template-columns: minmax(0, 1fr) auto 3.2rem; column-gap: .6rem; row-gap: .25rem;
    align-items: center; padding: .35rem .45rem; border-radius: var(--radius-sm);
    color: var(--text-primary); text-decoration: none; font-size: .8rem;
}
.band-row:hover, .band-row:focus { background: var(--surface-muted); }
.band-row-name { font-weight: 600; min-width: 0; }
.band-row-figure { font-weight: 700; text-align: right; white-space: nowrap; }
.band-row-pct { color: var(--text-secondary); font-size: .72rem; text-align: right; }
.band-row-track { grid-column: 1 / -1; height: 5px; border-radius: 3px; background: var(--border); overflow: hidden; }
.band-row-fill { height: 100%; border-radius: 3px; }
/* The summary toolbar's figures: an even grid that spreads across the bar
   and wraps a whole figure at a time, so no row ends in a gap and no divider
   is left dangling at a wrap. Two named groups (`.tb-groups` of `.tb-group`)
   sit side by side from xxl. */
.tb-groups { display: grid; gap: 1rem 2rem; }
.tb-metrics {
    display: grid; gap: .9rem 1.25rem;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
}
@media (min-width: 1400px) {
    .tb-groups { grid-template-columns: 4fr 5fr; }
    .tb-group + .tb-group { border-left: 1px solid var(--border); padding-left: 2rem; }
}
.dash-full { min-width: 0; }

@media (min-width: 576px) {
    .dash-parent-right { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .dash-right-cell { grid-column: 1 / -1; }
}
@media (min-width: 1200px) {
    /* 7/5 rather than 8/4: measured, the right parent carries five surfaces to
       the left parent's three, so the narrower half needs the extra column to
       finish alongside rather than far below. */
    .dash-shell { grid-template-columns: 7fr 5fr; }
    .dash-full  { grid-column: 1 / -1; }
}


/* ==========================================================================
   DASHBOARD SECTIONS AND CHARTS
   What a dashboard card is made of besides its rows (Docs/ui-canon.md 1a, 4b):
   the one quiet link from a section header to its full report, a "+ N more"
   line that is a link rather than dead text, the full-width pair, and the
   donut-with-legend a chart is drawn as. Behaviour is static/js/dash-charts.js;
   every colour is a key of config.colors, painted by that script.
   ========================================================================== */
.dash-view-link { font-size: .72rem; color: var(--brand-dark); text-decoration: none; white-space: nowrap; }
.dash-view-link:hover, .dash-more-row:hover, .dash-more-link:hover { text-decoration: underline; }
.dash-more-row { color: var(--brand-dark); font-size: .72rem; }
.dash-more-link { display: block; text-align: right; padding: .25rem .75rem .5rem; font-size: .72rem; color: var(--brand-dark); text-decoration: none; }
/* Two cards that answer one question side by side from lg, stacked below.
   Aligned to the top: a pair is chosen so the two run close in height, and a
   stretched card would only move the gap inside it. */
.dash-pair { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr); align-items: start; }
@media (min-width: 992px) { .dash-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 992px) { .dash-pair.is-5-7 { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); } }
/* A donut beside its legend. The legend states every figure in words, so the
   chart is never the only place a number can be read, and each row is a link. */
.chart-donut { display: grid; grid-template-columns: minmax(0, 11rem) minmax(0, 1fr); gap: 1rem; align-items: center; }
@media (max-width: 420px) { .chart-donut { grid-template-columns: minmax(0, 1fr); } }
.chart-legend-row {
    display: grid; grid-template-columns: .65rem minmax(0, 1fr) auto 3rem; column-gap: .5rem; align-items: center;
    padding: .3rem .4rem; border-radius: var(--radius-sm); color: var(--text-primary); text-decoration: none; font-size: .8rem;
}
.chart-legend-row:hover { background: var(--surface-muted); }
.chart-legend-key { width: .65rem; height: .65rem; border-radius: 2px; }
.chart-legend-name { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-legend-count { font-weight: 700; text-align: right; }
.chart-legend-pct { color: var(--text-secondary); font-size: .72rem; text-align: right; }
.chart-legend-sub { grid-column: 2 / -1; color: var(--text-secondary); font-size: .7rem; margin-top: -.15rem; }
.chart-tip { padding: .45rem .6rem; font-size: .75rem; line-height: 1.5; }
.chart-tip-key { display: inline-block; width: .55rem; height: .55rem; border-radius: 2px; margin-right: .35rem; }
.chart-tip-hint { color: var(--text-secondary); font-size: .68rem; margin-top: .2rem; }
/* The keyboard twin of a chart's clicks: hidden until a link takes focus,
   then shown in place with a visible ring. */
.chart-links { display: flex; flex-wrap: wrap; gap: .25rem; }
.chart-link:focus { position: static !important; width: auto !important; height: auto !important;
    margin: 0 !important; clip: auto !important; overflow: visible !important; white-space: normal;
    padding: .2rem .45rem; border-radius: var(--radius-sm); font-size: .75rem;
    background: var(--surface-muted); color: var(--text-primary);
    outline: 2px solid var(--brand); outline-offset: 1px; }


/* ==========================================================================
   LIST CARD
   A long list on a dashboard, kept to a card: a search box, filter pills that
   carry their own counts, a table whose header stays put while its body
   scrolls, and one link out to the full report. `Docs/ui-canon.md` section 3b
   owns when to use it; `static/js/table-filter.js` owns the behaviour. Main units
   available (Inventory and Account dashboards) and the Service Calls
   dashboard's lists are all this one part, so none of them can drift.

   Colours are literal where the card must render identically under any
   page's token block. `#e8f5e9` is the solid brand tint the dashboards
   shipped with; tokens.css's `--brand-light` is translucent and would not
   match.
   ========================================================================== */
/* A grid item defaults to min-width:auto and refuses to shrink below its
   content; inside a dashboard's grid column the table would then force the
   card wider than its column and bleed into the next one. */
.list-card-block { min-width: 0; }
.list-card {
    border-radius: var(--radius);
    border: 1px solid var(--border);
    border-left: 3px solid var(--brand);
    background: var(--surface);
    box-shadow: 0 1px 3px rgba(46,125,50,.05), 0 1px 2px rgba(0,0,0,.03);
}
/* Two rows, not one crowded one: search takes the icon's whole remaining
   width, and the pills get their own row and wrap freely. */
.list-card-header {
    display: flex;
    flex-direction: column;
    gap: .6rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--border);
}
/* Icon, title and search on one row; the search takes what the title leaves
   and drops to its own line before it gets too narrow to type in. */
.list-card-top { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
.list-card-icon {
    width: 1.8rem;
    height: 1.8rem;
    border-radius: .4rem;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .8rem;
    background: #e8f5e9;
    color: var(--brand);
}
.list-card-title { font-size: .85rem; font-weight: 700; color: var(--text-primary); white-space: nowrap; }
.list-card-pills { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.list-card-pill {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    border: 1px solid var(--border);
    background: var(--surface);
    border-radius: 2rem;
    padding: .3rem .8rem;
    font-size: .78rem;
    font-weight: 600;
    color: var(--text-secondary);
    cursor: pointer;
    transition: background .15s, color .15s, border-color .15s;
}
.list-card-pill i { font-size: .85rem; }
.list-card-pill:hover { background: var(--surface-muted); }
.list-card-pill:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.list-card-pill.active { background: #e8f5e9; border-color: var(--brand); color: var(--brand-dark); }
.list-card-count { opacity: .75; margin-left: .1rem; }
.list-card-search-wrap { position: relative; flex: 1 1 12rem; min-width: 0; }
.list-card-search-wrap i {
    position: absolute; left: .65rem; top: 50%; transform: translateY(-50%);
    font-size: .8rem; color: var(--text-secondary); pointer-events: none;
}
.list-card-search {
    width: 100%;
    padding: .3rem .6rem .3rem 1.9rem;
    font-size: .78rem;
    border-radius: 2rem;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-primary);
}
.list-card-search:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 .15rem #e8f5e9; }
.list-card-body { max-height: 26rem; overflow-y: auto; }
.list-card-table { font-size: .82rem; }
.list-card-table thead { background: var(--surface-muted); position: sticky; top: 0; z-index: 1; }
.list-card-table th {
    font-size: .65rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .05em; color: var(--text-secondary);
}
.list-card-strong { color: var(--text-primary); font-weight: 600; text-decoration: none; }
.list-card-muted { color: var(--text-secondary); }
/* A second, smaller line inside a cell: an identifier or a count that
   qualifies the row without earning a column of its own. */
.list-card-sub { font-size: .7rem; color: var(--text-secondary); }
.list-card-figure { font-weight: 700; color: var(--brand-dark); }
.list-card-info {
    color: var(--bs-secondary-color);
    font-size: .8rem;
    cursor: help;
    vertical-align: middle;
}
.list-card-note {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .5rem .75rem;
    font-size: .78rem;
    color: var(--bs-secondary-color);
    border-top: 1px solid var(--bs-border-color);
}
.list-card-empty {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 1.5rem 1rem; color: var(--text-secondary); font-size: .8rem;
}
.list-card-empty i { font-size: 1.4rem; margin-bottom: .35rem; opacity: .45; }
/* A failed read must not look like an empty one (ui-canon 5). */
.list-card-empty.is-failed { color: var(--red, #d32f2f); }
.list-card-empty.is-failed i { opacity: .8; }
/* Shown by table-filter.js only when a search leaves nothing in view, so an
   empty search is never mistaken for an empty list. */
.list-card-nomatch { padding: 1rem; text-align: center; font-size: .8rem; color: var(--text-secondary); }
.list-card-footer {
    text-align: right;
    padding: .6rem 1rem;
    border-top: 1px solid var(--border);
    background: var(--surface);
}
.list-card-link { color: var(--brand-dark); text-decoration: none; font-size: .78rem; }
/* A list card that takes up its column's slack (`.is-fill` inside a
   `.dash-fill` column, Docs/ui-canon.md 3b): from xl, where the shell's two
   columns sit side by side, the column becomes a flex column and only the
   fill card grows, its body scrolling inside it, so the shorter column shows
   more rows instead of ending in a gap. The cards above and below it keep
   their own height, which a stretched grid row would not. Below xl the
   columns stack and the card keeps its normal height. */
@media (min-width: 1200px) {
    .dash-fill { display: flex; flex-direction: column; }
    .list-card-block.is-fill { flex: 1 1 auto; display: flex; flex-direction: column; }
    .list-card-block.is-fill .list-card { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
    .list-card-block.is-fill .list-card-body { flex: 1 1 0; max-height: none; min-height: 18rem; }
}


/* ==========================================================================
   LIVE METER FROM EKM
   One compact pill directly under the header's identity badges, in both the
   mobile and desktop header blocks. It is contained rather than loose text so
   the eye catches it as a single object instead of reading it as a second row
   of badges: the caption names the source once, the figures carry the page's
   accent colours, and thin rules separate them so three numbers do not run
   together. The failure treatment is deliberately distinct from the neutral
   one, because a strip that reads as "nothing here" when EKM is unreachable is
   a lie nobody investigates.
   ========================================================================== */
.live-meter-strip {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem .8rem;
    margin-top: .5rem;
    padding: .3rem .75rem;
    border: 1px solid var(--border);
    /* **Not a pill.** A 999px radius only reads as intentional on a single
       line, and this strip wraps at every width the page is used at: two rows
       at 1440 once the reading time joins the three figures, four on a phone.
       Curved ends round two rows of square content look like a mistake. The
       panel is still unmistakably not a billing meter -- it carries the
       "Live meter from EKM" caption, its own tint and its own border. */
    border-radius: var(--radius);
    background: var(--surface-muted);
    font-size: .75rem;
    line-height: 1.5;
}
.live-meter-title {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    font-size: .65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--brand-dark);
}
.live-meter-help { cursor: help; color: var(--text-muted); }

.live-meter-item {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    color: var(--text-secondary);
}
.live-meter-item + .live-meter-item {
    border-left: 1px solid var(--border);
    padding-left: .8rem;
}
.live-meter-item strong {
    font-size: .85rem;
    font-weight: 700;
    color: var(--text-primary);
}
.lm-bw    { color: var(--slate); }
.lm-color { color: var(--teal); }
.lm-total { color: var(--brand); }

.live-meter-stamp { color: var(--text-muted); }

/* A failed read tints the whole pill, not just its text: the point of the
   distinction is that somebody notices it without reading. */
.live-meter-strip.is-failed {
    border-color: var(--amber);
    background: var(--amber-light);
}
.live-meter-note { display: inline-flex; align-items: center; gap: .35rem; }
.live-meter-note.is-failed { color: var(--amber); font-weight: 600; }
/* A saved reading is real data, not a failure, so it is muted rather than
   amber -- the title already says it is not live. */
.live-meter-note.is-saved  { color: var(--text-muted, #6c757d); }

/* **The pill is a pill only while it fits on one line.** Wrapped onto four
   rows on a phone, a 999px radius turns the block into a lozenge with curved
   ends round square content, the `inline-flex` width leaves a ragged right
   edge, and the dividers between figures land at the START of each wrapped
   row where they separate nothing.

   Below `lg` it becomes what it actually is at that width: a small panel. The
   figures pair off into two columns so B/W sits beside Color and the reading
   time gets its own row. */
@media (max-width: 991.98px) {
    .live-meter-strip {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: .3rem .75rem;
        width: 100%;
        padding: .5rem .75rem;
    }
    .live-meter-title,
    .live-meter-stamp,
    .live-meter-note { grid-column: 1 / -1; }
    /* The divider separated two figures on one line; stacked, it draws a line
       down the middle of nothing. */
    .live-meter-item + .live-meter-item {
        border-left: 0;
        padding-left: 0;
    }
}



/* ==========================================================================
   EQUIPMENT DETAIL
   **Every rule here is scoped to `.equipment-detail-container`.** The page had
   26 distinct type sizes and 688 elements under 12px, and the classes carrying
   them are shared with other Bridge surfaces, so the floor is raised here and
   nowhere else.

   The page was thirteen identical white cards in a single column: a reader
   scanning it found AR summary and Contract history weighted the same. Nothing
   is deleted to fix that, because every section earns its place. Instead the
   page is given a rhythm -- a heading that is a real heading, a quieter card
   chrome, and grouped spacing -- so the eye can tell a primary record from
   supporting context without the content changing.
   ========================================================================== */
.equipment-detail-container {
    /* One scale, replacing seven arbitrary steps between 8.8px and 11.5px.
       12px is the floor: nothing on this page renders smaller. */
    --eq-fs-xs: .75rem;      /* 12px -- badges, chips, table meta          */
    --eq-fs-sm: .8125rem;    /* 13px -- secondary text, table cells        */
    --eq-fs-md: .875rem;     /* 14px -- body                               */
    --eq-fs-lg: 1rem;        /* 16px -- section headings                   */
    --eq-gap: 1rem;
}

/* ---- Section headings ---------------------------------------------------
   They were `<h6>` carrying no heading weight and skipping h2 through h5, so
   the document outline read H1 then eleven H6s. They are `<h2>` now; this
   gives them the size the old h6 had in practice, so the change is semantic
   and not a sudden jump in scale. */
.equipment-detail-container .eq-section-title {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    font-size: var(--eq-fs-lg);
    font-weight: 600;
    line-height: 1.3;
    color: var(--text-primary);
}
.equipment-detail-container .eq-section-title .badge { font-weight: 600; }

/* ---- Type floor ---------------------------------------------------------
   The offenders are class-driven and those classes are shared, so they are
   raised INSIDE this page only. Each one was measured on the rendered page
   before being listed. */
.equipment-detail-container .snapshot-label,
.equipment-detail-container .ekm-status-head,
.equipment-detail-container .ekm-other-summary,
.equipment-detail-container .live-meter-title,
.equipment-detail-container .card-meta,
.equipment-detail-container .card-detail-label { font-size: var(--eq-fs-xs); }

.equipment-detail-container .ekm-status-facts,
.equipment-detail-container .ekm-toner-est,
.equipment-detail-container .ekm-toner-read,
.equipment-detail-container .ekm-toner-pct.is-muted,
.equipment-detail-container .ekm-alert-since,
.equipment-detail-container .ekm-other-meta,
.equipment-detail-container .ekm-other-level.is-muted,
.equipment-detail-container .tf-count,
.equipment-detail-container .tf-search,
.equipment-detail-container small,
.equipment-detail-container .small { font-size: var(--eq-fs-sm); }

.equipment-detail-container .ekm-alert-tag,
.equipment-detail-container .ekm-monitor-pill,
.equipment-detail-container .filter-chip,
.equipment-detail-container .chip-count { font-size: var(--eq-fs-xs); }

/* Every button on the page is `.btn-sm`, which resolves to 11.2px: the header
   actions, "View full contract", "View AR details" and the row actions inside
   the mobile cards. They are the page's primary controls and were the
   smallest text on it. */
.equipment-detail-container .btn-sm { font-size: var(--eq-fs-sm); }

.equipment-detail-container .data-table th {
    font-size: var(--eq-fs-xs);
    letter-spacing: .02em;
}
.equipment-detail-container .data-table td { font-size: var(--eq-fs-sm); }

/* The table head is set by `.data-table thead th { font-size: .68rem
   !important }`, so it stayed at 10.88px while everything around it moved to
   12. The `!important` is matched rather than removed: that rule is shared
   with every other report table and this page is not the place to change
   them.

   **The colour is left exactly as it is.** `--text-secondary` on the head's
   `#f6f8f6` measures about 4.73:1, which clears AA for normal text, so the
   readability problem here was the 10.88px size and not the pair. */
.equipment-detail-container .data-table thead th,
.equipment-detail-container .table thead th {
    font-size: var(--eq-fs-xs) !important;
}

/* ---- Bounded tables -----------------------------------------------------
   Two tables had no ceiling: service history and the same-model list each ran
   492px and grew with the data, while the two beside them were already capped
   at 26rem. One rule, so a long history cannot push the rest of the page off
   the screen and every table on the page behaves alike. The head stays put,
   because a date column with no heading three screens down is unreadable. */
.equipment-detail-container .data-table-wrap {
    max-height: 26rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.equipment-detail-container .data-table-wrap thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--surface);
}
/* **The frozen first column has to outrank the header it sits under.**
   `.data-table thead th:first-child` gives it `z-index: 3` and its own
   background, but the rule above has the same specificity and comes later in
   the file, so it was winning and dropping that cell to `z-index: 2`. Tied
   with the body cells, the later DOM painted over it and the customer column
   scrolled straight through the header. Restated here rather than reordered,
   because the shared rule serves every other report table. */
.equipment-detail-container .data-table-wrap thead th:first-child {
    left: 0;
    z-index: 4;
    background: #f6f8f6;
}
.equipment-detail-container .data-table-wrap tbody td:first-child {
    z-index: 1;
}
@media (max-width: 767.98px) {
    .equipment-detail-container .data-table-wrap { max-height: 22rem; }
}

/* ---- Card chrome --------------------------------------------------------
   Thirteen cards with the same border, shadow and header fill made every
   section shout equally. The shell is quietened so the HEADING carries the
   hierarchy: a hairline, a flat header, and the shadow kept only where a card
   is genuinely raised above the page. */
.equipment-detail-container .card.border-0.shadow-sm {
    border: 1px solid var(--border) !important;
    box-shadow: none !important;
    border-radius: var(--radius);
}
.equipment-detail-container .card-header.bg-light {
    background: var(--surface) !important;
    border-bottom: 1px solid var(--border) !important;
    padding: .7rem .9rem;
}
/* The three records a reader came for keep a little more presence than the
   supporting context below them. */
.equipment-detail-container .eq-primary .card.border-0.shadow-sm {
    box-shadow: var(--shadow-xs) !important;
}

/* The EKM card's three signals started hard against the header's rule, so the
   column captions read as part of the card title rather than as the start of
   the content. */
.equipment-detail-container .ekm-status-grid { padding-top: .35rem; }

/* ---- Rhythm -------------------------------------------------------------
   Sections were separated by a uniform 1.5rem, so nothing grouped. Related
   cards now sit closer together than unrelated subjects do, which is what
   tells a reader where one subject ends without a label having to say it. */
.equipment-detail-container .eq-group { margin-bottom: 1.75rem; }
.equipment-detail-container .eq-group > .row { --bs-gutter-y: 1rem; }
/* ---- A credit, wherever it is shown ------------------------------------- */
.equipment-detail-container .eq-credit-tag {
    display: inline-block;
    margin-left: .35rem;
    padding: .05rem .4rem;
    border-radius: 999px;
    background: var(--primary-green-extra-light);
    color: var(--primary-green-dark);
    font-size: var(--eq-fs-xs);
    font-weight: 700;
    vertical-align: middle;
}

/* ---- Figures that change without a reload -------------------------------
   The live meter, the toner percentages and the row counts are all written by
   script after the page has painted, and several of them change again as a
   reader filters. Proportional digits are different widths, so every one of
   those updates nudged the text beside it. */
.equipment-detail-container .snapshot-value,
.equipment-detail-container .live-meter-item strong,
.equipment-detail-container .ekm-toner-pct,
.equipment-detail-container .ekm-other-level,
.equipment-detail-container .tf-count,
.equipment-detail-container .chip-count,
.equipment-detail-container .data-table td {
    font-variant-numeric: tabular-nums;
}

/* ---- Pointer targets ----------------------------------------------------
   The chips, the search box and the disclosure were 28 to 29px tall. Raised
   by padding rather than by a pseudo-element: these chips wrap onto three
   rows with a 4.8px gap, and an invisible 40px target on each would have
   overlapped the row above.

   36px is a deliberate compromise. The guideline for a dense desktop surface
   is 40px, but this is a wrapped multi-row filter inside a card and 40 pushed
   the row 33px taller for no gain in accuracy; touch gets the full 44. */
.equipment-detail-container .filter-chip {
    min-height: 36px;
    display: inline-flex;
    align-items: center;
}
/* The summary keeps `display: list-item` so it keeps its disclosure triangle.
   Setting `inline-flex` here to centre the label removed the marker, which is
   the only thing on the row saying it opens. */
.equipment-detail-container .ekm-other-summary {
    min-height: 36px;
    display: list-item;
    padding-top: .55rem;
}
.equipment-detail-container .tf-search { min-height: 36px; }
@media (pointer: coarse) {
    .equipment-detail-container .filter-chip,
    .equipment-detail-container .tf-search { min-height: 44px; }
    .equipment-detail-container .ekm-other-summary {
        min-height: 44px;
        padding-top: .8rem;
    }
}

/* ---- Focus -------------------------------------------------------------
   The chips, disclosures and search boxes are all keyboard reachable and none
   of them showed where the keyboard was. */
.equipment-detail-container a:focus-visible,
.equipment-detail-container button:focus-visible,
.equipment-detail-container summary:focus-visible,
.equipment-detail-container input:focus-visible,
.equipment-detail-container [tabindex]:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}
.equipment-detail-container .mobile-card:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

/* ---- Touch targets ------------------------------------------------------ */
@media (max-width: 767.98px) {
    .equipment-detail-container .filter-chip { padding: .35rem .6rem; }
    .equipment-detail-container .tf-search { padding-top: .4rem; padding-bottom: .4rem; }
    .equipment-detail-container .eq-section-title { font-size: var(--eq-fs-md); }
}

/* Motion is decorative here, so it yields. */
@media (prefers-reduced-motion: reduce) {
    .equipment-detail-container * {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
    }
}


/* ==========================================================================
   SECTION FILTER PANEL
   The search box, chip row and matching count that sit in a card header above
   a bounded table, driven by static/js/table-filter.js. Written once because
   two sections on the equipment detail page need the same thing and the Main
   Units card already proved the shape works.

   The chips themselves are `.filter-chip`, the report's own component, rather
   than a third set of pill styles. Only the layout around them is new.

   The table below sits in `.tf-scroll`, capped in height, so a machine with a
   long history does not push the rest of the page off the screen. The cap is
   in rows rather than pixels by intent: it is tuned so the reader can see the
   table has more in it without scrolling the page to find that out.
   ========================================================================== */
.tf-panel {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem .75rem;
    width: 100%;
}
.tf-search-wrap {
    position: relative;
    flex: 1 1 14rem;
    min-width: 0;
    max-width: 22rem;
}
.tf-search-wrap > i {
    position: absolute;
    top: 50%;
    left: .6rem;
    transform: translateY(-50%);
    font-size: .9rem;
    color: var(--text-muted);
    pointer-events: none;
}
.tf-search {
    width: 100%;
    padding: .3rem .6rem .3rem 1.9rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    font-size: .78rem;
    color: var(--text-primary);
}
.tf-search:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 .15rem var(--brand-extra-light);
}
.tf-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .3rem;
    min-width: 0;
}
/* The count is a live region, so a keyboard or screen-reader user is told the
   result of a filter they cannot see change. */
.tf-count {
    margin-left: auto;
    font-size: .72rem;
    font-weight: 600;
    color: var(--text-muted);
    white-space: nowrap;
}
.tf-scroll {
    max-height: 26rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}
/* The head stays put while the body scrolls: a date column with no heading
   three screens down is unreadable. */
.tf-scroll thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--surface);
}
.tf-empty {
    padding: 1.5rem 1rem;
    text-align: center;
    font-size: .8rem;
    color: var(--text-muted);
}
.tf-note {
    padding: .5rem .85rem;
    border-top: 1px solid var(--border);
    font-size: .72rem;
    color: var(--amber-ink);
    background: var(--amber-light);
}
@media (max-width: 575.98px) {
    .tf-search-wrap { flex-basis: 100%; max-width: none; }
    .tf-count { margin-left: 0; }
    .tf-scroll { max-height: 22rem; }
}


/* ==========================================================================
   EKM DEVICE STATUS
   One full-width card under the summary cards, carrying every EKM signal
   about a machine except the live meter, which stays in the header beside the
   identity. The grid is columns at lg and a stack below it, and it is written
   as auto-fit rather than two fixed columns so the reserved alerts column
   drops in later without either of these two moving or the file changing.

   Everything here is deliberately quiet. A vendor signal that is missing or
   unreadable is one line, because this card sits above the equipment and
   contract detail a reader came for, and an EKM outage must not be the
   loudest thing on the page. Amber marks only a data condition somebody has
   to fix; a machine EKM simply does not know is neutral.
   ========================================================================== */
.ekm-status-grid {
    display: grid;
    gap: 1rem 1.5rem;
    grid-template-columns: 1fr;
}
@media (min-width: 992px) {
    /* **Proportional to the content, not equal.** Three equal columns gave
       monitoring and alerts a third of the card each for three lines of text,
       and the toner list a third for twelve. The two short signals are stacked
       in the first column and the supplies list takes the wider second. */
    .ekm-status-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); }
}
/* The stacked pair reads as two blocks, separated by a rule rather than by
   the empty space that used to sit under each of them. */
.ekm-status-stack { display: flex; flex-direction: column; gap: 1rem; }
.ekm-status-stack > .ekm-status-col + .ekm-status-col {
    padding-top: 1rem;
    border-top: 1px solid var(--border);
    padding-left: 0;
    border-left: 0;
}
@media (min-width: 992px) {
    .ekm-status-stack > .ekm-status-col + .ekm-status-col {
        padding-left: 0;
        border-left: 0;
        padding-top: 1rem;
        border-top: 1px solid var(--border);
    }
}
.ekm-status-col + .ekm-status-col {
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}
@media (min-width: 992px) {
    .ekm-status-col + .ekm-status-col {
        padding-top: 0;
        padding-left: 1.5rem;
        border-top: 0;
        border-left: 1px solid var(--border);
    }
}
.ekm-status-head {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin-bottom: .5rem;
    font-size: .65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--brand-dark);
}
.ekm-status-help { cursor: help; color: var(--text-muted); }
.ekm-status-body { font-size: .8rem; line-height: 1.55; }
.ekm-status-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem .6rem;
}
.ekm-status-line strong { color: var(--text-primary); }
.ekm-status-facts { color: var(--text-secondary); font-size: .75rem; }
.ekm-status-muted { color: var(--text-muted); }
.ekm-status-note { color: var(--text-secondary); }
.ekm-status-note.is-attention { color: var(--amber-ink); font-weight: 600; }

/* The contact pill. Online is the healthy state and the only one that earns a
   positive colour; unknown is muted rather than amber, because not having been
   told is not the same as being told something bad. Every pill also says its
   state in words, so the card reads correctly with no colour at all. */
.ekm-monitor-pill {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .05rem .5rem;
    border-radius: 999px;
    font-size: .68rem;
    font-weight: 700;
}
.ekm-monitor-pill.is-online  { background: var(--primary-green-extra-light); color: var(--primary-green-dark); }
.ekm-monitor-pill.is-offline { background: var(--amber-light); color: var(--amber-ink); }
.ekm-monitor-pill.is-unknown { background: var(--surface-muted); color: var(--text-muted); border: 1px solid var(--border); }

/* One toner per row: swatch, colour, bar, figure, estimate, reading age. The
   bar is a second encoding of the percentage that is already printed beside
   it, never the only one, so a reader who cannot judge the fill still has the
   number. A supply with no percentage draws an empty track rather than a full
   or an empty bar, both of which would state something the device did not. */
.ekm-toner-row {
    display: grid;
    /* **One line per cartridge.** Each toner used three lines -- name and bar,
       then the estimate, then a reading time identical to the three above it --
       so four cartridges filled twelve lines and the panel read as a wall. The
       estimate now sits on the same line and the shared reading time is
       printed once beneath the group.

       The track is capped rather than given the leftover space: stretched
       across a wide column a 1% fill becomes a hairline, which is the reading
       that most needs to be legible. */
    grid-template-columns: auto minmax(3.5rem, auto) minmax(3rem, 7rem) 2.5rem 1fr;
    align-items: center;
    gap: .15rem .5rem;
    padding: .3rem 0;
}
.ekm-toner-row + .ekm-toner-row { border-top: 1px solid var(--border); }
.ekm-toner-dot {
    width: .6rem;
    height: .6rem;
    border-radius: 50%;
    border: 1px solid var(--border);
}
.ekm-toner-dot.is-black   { background: #212529; }
.ekm-toner-dot.is-cyan    { background: #00b0d7; }
.ekm-toner-dot.is-magenta { background: #d6009a; }
.ekm-toner-dot.is-yellow  { background: #f2c200; }
.ekm-toner-dot.is-other   { background: var(--surface-muted); }
.ekm-toner-name { font-weight: 600; color: var(--text-primary); }
.ekm-toner-track {
    position: relative;
    height: .45rem;
    min-width: 3rem;
    border-radius: 999px;
    background: var(--surface-muted);
    overflow: hidden;
}
.ekm-toner-track.is-empty { background: transparent; border: 1px dashed var(--border); }
.ekm-toner-fill { display: block; height: 100%; border-radius: 999px; }
.ekm-toner-fill.is-black   { background: #212529; }
.ekm-toner-fill.is-cyan    { background: #00b0d7; }
.ekm-toner-fill.is-magenta { background: #d6009a; }
.ekm-toner-fill.is-yellow  { background: #f2c200; }
.ekm-toner-fill.is-other   { background: var(--slate); }
.ekm-toner-pct { font-weight: 700; color: var(--text-primary); text-align: right; }
.ekm-toner-pct.is-low    { color: var(--amber-ink); }
.ekm-toner-pct.is-muted  { font-weight: 500; color: var(--text-muted); font-size: .72rem; }
/* The estimate rides on the same line as the figure it qualifies. The reading
   time only appears per row where that row disagrees with the others, and then
   it takes its own line. */
.ekm-toner-est {
    justify-self: end;
    font-size: .7rem;
    color: var(--text-muted);
    white-space: nowrap;
}
.ekm-toner-read {
    grid-column: 2 / -1;
    font-size: .7rem;
    color: var(--text-muted);
}
/* Below the pair breakpoint the row has less width than it needs, so the
   estimate drops under the bar rather than squeezing the percentage. */
@media (max-width: 400px) {
    .ekm-toner-row { grid-template-columns: auto minmax(3.5rem, auto) 1fr 2.5rem; }
    .ekm-toner-est { grid-column: 2 / -1; justify-self: start; }
}
.ekm-toner-read.is-stale { color: var(--amber-ink); }
/* An undated reading is neither fresh nor stale: nobody said when it was
   taken. Muted rather than amber, because amber is for a level that IS known
   to be old and this one is not a finding, only an absence. */
.ekm-toner-read.is-undated { color: var(--text-muted); font-style: italic; }


/* One alert per row: what it is, what the machine says, and since when. The
   tag states the grouping in words, so the colour repeats the meaning rather
   than carrying it, and nothing here says "dispatch": EKM publishes no
   severity scale, so the strongest honest claim is that somebody has to act. */
.ekm-alert-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .5rem;
    padding: .35rem 0;
}
.ekm-alert-row + .ekm-alert-row { border-top: 1px solid var(--border); }
.ekm-alert-tag {
    flex: 0 0 auto;
    padding: .05rem .45rem;
    border-radius: 999px;
    background: var(--surface-muted);
    border: 1px solid var(--border);
    font-size: .62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--text-muted);
}
.ekm-alert-tag.is-attention {
    background: var(--amber-light);
    border-color: var(--amber);
    color: var(--amber-ink);
}
.ekm-alert-reason {
    flex: 1 1 8rem;
    min-width: 0;
    font-weight: 600;
    color: var(--text-primary);
}
.ekm-alert-since {
    flex: 1 1 100%;
    font-size: .7rem;
    color: var(--text-muted);
}


/* Other supplies: drums, the waste collector, fusers and maintenance parts,
   behind a `<details>` so the default column stays short. A real disclosure
   rather than a scripted toggle, for the reason ui-canon 2d gives: it opens
   with the keyboard and works with JavaScript off.

   Quieter than the toner rows on purpose. These are parts a reader consults,
   not levels they act on, and nothing here ranks them: a waste figure could
   count either way, so sorting by percentage would assert what this feature
   refuses to assert. */
.ekm-other { margin-top: .5rem; border-top: 1px solid var(--border); }
.ekm-other-summary {
    padding: .4rem 0 .3rem;
    cursor: pointer;
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted);
    list-style: revert;
}
.ekm-other-summary:hover { color: var(--brand-dark); }
.ekm-other[open] .ekm-other-summary { color: var(--brand-dark); }
.ekm-other-row {
    display: grid;
    grid-template-columns: minmax(6rem, 1fr) auto;
    gap: .1rem .6rem;
    padding: .3rem 0;
    font-size: .75rem;
}
.ekm-other-row + .ekm-other-row { border-top: 1px solid var(--border); }
.ekm-other-name { font-weight: 600; color: var(--text-primary); }
/* A type the application does not recognise is marked, so a reader knows the
   name came from EKM rather than from Bridge. */
.ekm-other-name.is-unknown-type { font-style: italic; color: var(--text-secondary); }
.ekm-other-level { text-align: right; font-weight: 700; color: var(--text-primary); }
.ekm-other-level.is-muted { font-weight: 500; font-size: .7rem; color: var(--text-muted); }
/* A figure with no agreed meaning is not styled like one that has one. */
.ekm-other-level.is-unlabelled { font-weight: 600; color: var(--text-secondary); }
.ekm-other-meta {
    grid-column: 1 / -1;
    font-size: .68rem;
    color: var(--text-muted);
}
.ekm-other-meta.is-stale { color: var(--amber-ink); }


/* ==========================================================================
   EKM DATA QUALITY FILTER ROW
   A second row inside the equipment report's filter card, below the main chip
   rail and shown only for print equipment. It is separate because the rail
   already scrolls: three more chips in it pushed the ones that matter out of
   sight, and these apply to one category rather than the whole report. The
   label says which system is speaking, so a reader knows these counts come
   from EKM rather than from the mirror the rest of the row describes.
   ========================================================================== */
.ekm-filter-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem .5rem;
    padding: .55rem .75rem 0;
    margin-top: .1rem;
    border-top: 1px solid var(--border);
}
.ekm-filter-label {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    margin-right: .25rem;
    font-size: .65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
}

.ekm-updated { font-size: .72rem; color: var(--text-muted, #6c757d); margin-left: .5rem; }
/* Stale is a state a reader must notice, so it takes the same amber the
   failed row state uses rather than staying muted. */
.ekm-updated.is-stale { color: var(--amber); font-weight: 600; }

/* ==================================================================
   EMAIL TEMPLATE EDITOR
   The surfaces at /system/email-templates. Values live here, never in
   the template (Rule 14c).
   ================================================================== */

/* Page-scoped tokens, every one from the shared palette, so this surface
   tracks the theme like the settings pages it is reached from. */
.tpl-surface {
    /* One height for the editing pane, the source and the preview, so the two
       columns end level however tall the message is. Every pane scrolls
       inside itself rather than growing the page. */
    --tpl-pane: clamp(440px, 58vh, 780px);
    --tpl-line  : var(--border);
    --tpl-panel : var(--surface);
    --tpl-alt   : var(--surface-muted);
    --tpl-ink   : var(--text-primary);
    --tpl-soft  : var(--text-secondary);
    --tpl-mute  : var(--text-muted);
}

/* -- State --------------------------------------------------------------- */
.state-pill {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: .15rem .55rem; border-radius: 999px;
    font-size: .72rem; font-weight: 600;
    background: var(--tpl-alt); color: var(--tpl-soft);
}
.state-pill.is-changed { background: var(--brand-light); color: var(--brand); }
.state-pill i { font-size: .8rem; }

/* -- Fields -------------------------------------------------------------- */
.tpl-field { padding: .85rem .9rem; }
.tpl-field + .tpl-field { border-top: 1px solid var(--tpl-line); }
.tpl-field label {
    display: block; font-size: .78rem; font-weight: 600; margin-bottom: .2rem;
}
.tpl-help { font-size: .73rem; color: var(--tpl-mute); margin: 0 0 .45rem; }

/* Source, not prose. A monospace face, no wrapping surprises, and enough
   height that a body is readable without scrolling a 4-line window. */
.tpl-code, .cm-editor {
    font-family: var(--font-mono, ui-monospace, Consolas, 'Courier New', monospace);
    font-size: .78rem; line-height: 1.55;
}
.tpl-code { white-space: pre; overflow: auto; height: var(--tpl-pane); }
.tpl-text { font-size: .8rem; height: var(--tpl-pane); }
/* CodeMirror, when it loads. The textarea below it is the fallback and looks
   the same, so a blocked CDN changes nothing but the highlighting. */
.cm-editor {
    border: 1px solid var(--tpl-line); border-radius: var(--radius-sm);
    max-height: 60vh;
}
.cm-editor.cm-focused { outline: 2px solid var(--brand-light); outline-offset: -1px; }
.cm-scroller { overflow: auto; }

/* -- Modes --------------------------------------------------------------- */
.tpl-modes {
    display: inline-flex; border: 1px solid var(--tpl-line);
    border-radius: var(--radius-sm); overflow: hidden;
}
.tpl-modes button {
    padding: .25rem .7rem; font-size: .74rem; font-weight: 600;
    background: none; border: 0; color: var(--tpl-soft);
}
.tpl-modes button.active { background: var(--brand); color: #fff; }

/* The card is one white surface. A tinted head on a tinted page reads as
   background, not as the top of a card. */
.tpl-surface .card { background: var(--surface); }
.tpl-surface .card-header {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: .7rem 1rem;
}

/* A failure is not an empty state and must not read like one. */
.tpl-failed {
    display: flex; align-items: flex-start; gap: .5rem;
    margin-top: .5rem; padding: .55rem .7rem; font-size: .82rem;
    border: 1px solid #f0c2c2; border-left: 3px solid #d9534f;
    border-radius: var(--radius-sm); background: #fdf3f3; color: #8a2b26;
}

/* Visual mode is the rendered message itself. The frame is a real email at
   its real width, so the shell only has to hold it still. */
.visual-shell {
    border: 1px solid var(--tpl-line); border-radius: var(--radius-sm);
    background: #fff; overflow: hidden;
}
.visual-frame {
    display: block; width: 100%; height: var(--tpl-pane);
    border: 0; background: #fff;
}

/* -- Validation ---------------------------------------------------------- */
.field-errors {
    padding: .45rem .65rem; margin-top: .45rem; border-radius: var(--radius-sm);
    background: var(--danger-light); color: var(--danger); font-size: .76rem;
}
.field-errors ul { margin: 0; padding-left: 1.05rem; }

/* -- Variables ----------------------------------------------------------- */
.var-list { padding: .4rem; max-height: 46vh; overflow-y: auto; }
.var-item {
    display: block; width: 100%; text-align: left; border: 1px solid transparent;
    background: none; border-radius: var(--radius-sm); padding: .4rem .5rem;
    cursor: pointer;
}
.var-item:hover, .var-item:focus-visible {
    background: var(--tpl-alt); border-color: var(--tpl-line);
}
.var-token {
    font-family: var(--font-mono, monospace); font-size: .76rem;
    font-weight: 600; color: var(--brand);
}
/* The word carries the state; the colour only reinforces it, so this survives
   a greyscale print and a colour-blind reader (UI canon section 3). */
.var-required {
    margin-left: .35rem; font-size: .64rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: .04em; color: var(--danger);
}
.var-desc { font-size: .72rem; color: var(--tpl-soft); }

/* -- Preview ------------------------------------------------------------- */
.tpl-preview-subject {
    padding: .5rem .9rem; border-bottom: 1px solid var(--tpl-line);
    font-size: .82rem; background: var(--tpl-alt);
}
.tpl-preview-subject strong { font-weight: 600; }
.tpl-tabs { display: flex; gap: .2rem; padding: 0 .5rem; border-bottom: 1px solid var(--tpl-line); }
.tpl-tabs button {
    padding: .45rem .75rem; font-size: .76rem; font-weight: 600; background: none;
    border: none; border-bottom: 2px solid transparent; color: var(--tpl-soft);
}
.tpl-tabs button.active { color: var(--brand); border-bottom-color: var(--brand); }
/* Sandboxed: email HTML renders, and cannot script this page. */
/* The two columns finish level. The editor side is the taller of the two by
   the subject field and the mode row, so the PREVIEW grows to meet it rather
   than both being given a height and hoping. */
.tpl-cols > [class*="col-"] { display: flex; flex-direction: column; }
#previewCard { flex: 1 1 auto; }
#previewCard, #previewHtmlWrap, #previewTextWrap {
    display: flex; flex-direction: column; min-height: 0;
}
#previewHtmlWrap, #previewTextWrap { flex: 1 1 auto; }
/* `display:flex` above would otherwise beat the `hidden` attribute the tab
   switch sets, and both panes would show at once. */
#previewHtmlWrap[hidden], #previewTextWrap[hidden] { display: none; }
.tpl-preview-frame {
    width: 100%; height: 100%; min-height: var(--tpl-pane);
    border: 0; background: #fff; display: block;
}
.tpl-preview-text {
    white-space: pre-wrap; font-family: var(--font-mono, monospace);
    font-size: .76rem; padding: .75rem; height: 58vh; overflow: auto; margin: 0;
}
.tpl-preview-state {
    padding: .4rem .9rem; font-size: .73rem; color: var(--tpl-mute);
    border-top: 1px solid var(--tpl-line);
}
.tpl-preview-state.is-stale { color: var(--danger); }

/* Below lg the two columns stack, so the preview follows the source instead
   of sitting in a column half a screen wide. */
@media (max-width: 991.98px) {
    .tpl-surface { --tpl-pane: clamp(360px, 46vh, 560px); }
    #previewCard { position: static; }
}
