/* Self-hosted InterVariable (brand spec) — replaces the Google Fonts import. */
@font-face {
    font-family: "InterVariable";
    src: url("/fonts/InterVariable.woff2") format("woff2");
    font-weight: 100 900;
    font-display: swap;
}

/* The SAME file, declared under the name Syncfusion asks for.
 *
 * Syncfusion's tailwind-dark theme opens with
 *   @import "https://fonts.googleapis.com/css2?family=Inter:...";
 * and its components then set font-family: Inter. That single line in a NuGet-shipped
 * stylesheet is the only reason the app's CSP carried fonts.googleapis.com in style-src and
 * fonts.gstatic.com in font-src — two third-party origins, on every admin page, to fetch a
 * typeface already sitting in /fonts as InterVariable.
 *
 * The import cannot be deleted (the file is a package asset and the build restores it), so the
 * request is made REDUNDANT instead: with "Inter" resolving locally, the browser has the family
 * before the remote sheet matters, and blocking the origins costs nothing visible.
 *
 * ⚠️ If a future Syncfusion theme asks for a different family, this stops covering it and the
 * components fall back to the system stack — check here before assuming the CSP is at fault.
 */
@font-face {
    font-family: "Inter";
    src: url("/fonts/InterVariable.woff2") format("woff2");
    font-weight: 100 900;
    font-display: swap;
}

:root {
    /* ── Canonical MOTLogs brand tokens (dark) — duplicated per repo, see brand spec ── */
    --ml-bg: #121212;
    --ml-surface-1: #1E1E1E;
    --ml-surface-2: #242424;
    --ml-surface-3: #2A2A2A;
    --ml-accent: #FF5432;
    --ml-accent-hover: #FF7A5E;
    --ml-accent-glow: rgba(255, 84, 50, 0.15);
    --ml-text: #FFFFFF;
    --ml-text-secondary: #A0A0A0;
    --ml-text-muted: #707070;
    --ml-border: rgba(255, 255, 255, 0.08);
    --ml-border-strong: rgba(255, 255, 255, 0.15);
    --ml-success: #26B050;
    --ml-warning: #F39C12;
    --ml-error: #E74C3C;
    --ml-info: #3B82F6;

    /* App tokens point at the canonical set. */
    --bg-primary: var(--ml-bg);
    --bg-surface: var(--ml-surface-1);
    --bg-elevated: var(--ml-surface-3);
    --accent: var(--ml-accent);
    --accent-hover: var(--ml-accent-hover);
    --accent-glow: var(--ml-accent-glow);
    --text-primary: var(--ml-text);
    --text-secondary: var(--ml-text-secondary);
    --text-muted: var(--ml-text-muted);
    --border: var(--ml-border);
    --border-strong: var(--ml-border-strong);
    --success: var(--ml-success);
    --warning: var(--ml-warning);
    --error: var(--ml-error);
    --info: var(--ml-info);
    --border-radius: 12px;
    --transition: all 0.2s ease;
}

html[data-theme="light"] {
    /* ── Canonical MOTLogs brand tokens (light) ── */
    --ml-bg: #F9FAFB;
    --ml-surface-1: #FFFFFF;
    --ml-surface-2: #F1F3F5;
    --ml-surface-3: #F1F3F5;
    --ml-accent: #CC3415;
    --ml-accent-hover: #A82A10;
    --ml-accent-glow: rgba(204, 52, 21, 0.10);
    --ml-text: #1A1A1A;
    --ml-text-secondary: #525252;
    --ml-text-muted: #8B8B8B;
    --ml-border: rgba(0, 0, 0, 0.08);
    --ml-border-strong: rgba(0, 0, 0, 0.15);
    --ml-success: #1D8A3D;
    --ml-warning: #C77A0D;
    --ml-error: #C0392B;
    --ml-info: #2563EB;
}

/* Bootstrap's .btn-outline-light is invisible on a light background —
   in light mode, paint it with dark text/border instead. */
html[data-theme="light"] .btn-outline-light {
    color: var(--text-primary);
    border-color: var(--border-strong);
}

/* ───── Agent setup progress ─────
   Renders on the Agent-line cards and in the tab row (/agent page). Replaces
   the old binary "Setup required" chip with a per-line % + per-tab red-asterisk
   flags. Tooltip on the progress widget (native title attribute) lists every
   missing/recommended item so the user doesn't have to click into the line to
   find out what's left. */
.setup-progress { user-select: none; cursor: help; }
.setup-progress-track {
    width: 100%;
    height: 4px;
    background: rgba(255, 255, 255, 0.08);
    border-radius: 2px;
    overflow: hidden;
}
html[data-theme="light"] .setup-progress-track {
    background: rgba(0, 0, 0, 0.08);
}
.setup-progress-fill {
    height: 100%;
    background: var(--accent);
    border-radius: 2px;
    transition: width 240ms ease;
}
.setup-progress-fill.ready { background: var(--success); }

.setup-asterisk {
    color: var(--error);
    font-weight: 700;
    font-size: 14px;
    line-height: 1;
    padding: 0 4px;
    cursor: help;
}

.tab-asterisk {
    color: var(--error);
    font-weight: 700;
    margin-left: 4px;
    font-size: 14px;
    line-height: 1;
    vertical-align: top;
}

/* ───── Auth-resolving splash ─────
   Rendered by <Authorizing> in App.razor while CustomAuthStateProvider fetches
   /api/auth/me. Full-viewport fixed overlay so it covers MainLayout chrome too
   (Router wraps Authorizing content in whatever layout applies). Mirrors the
   pre-Blazor #boot splash in wwwroot/index.html so the transition from
   marketing click → Blazor hydrate → auth check → login page is one continuous
   dark frame with no flashes. */
.auth-splash {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
    background: #121212;
    color: #FFFFFF;
    z-index: 9999;
    font-family: "InterVariable","Inter",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
html[data-theme="light"] .auth-splash { background: #F9FAFB; color: #1A1A1A; }

.auth-splash-brand {
    font-size: 1.75rem;
    font-weight: 800;
    letter-spacing: 0.02em;
}
.auth-splash-brand .a { color: #FF5432; }

.auth-splash-bar {
    width: 160px;
    height: 2px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.08);
    border-radius: 2px;
}
html[data-theme="light"] .auth-splash-bar { background: rgba(0, 0, 0, 0.08); }

.auth-splash-bar > span {
    display: block;
    width: 40%;
    height: 100%;
    background: #FF5432;
    animation: auth-splash-slide 1.2s ease-in-out infinite;
}
@keyframes auth-splash-slide {
    0%   { margin-left: -40%; }
    100% { margin-left: 100%; }
}
@media (prefers-reduced-motion: reduce) {
    .auth-splash-bar > span { animation: none; margin-left: 0; width: 100%; }
}
html[data-theme="light"] .btn-outline-light:hover {
    background: var(--bg-elevated);
    color: var(--text-primary);
    border-color: var(--border-strong);
}

html, body {
    font-family: "InterVariable","Inter",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
    background-color: var(--bg-primary);
    color: var(--text-primary);
    margin: 0;
    padding: 0;
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
    font-family: "InterVariable","Inter",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
    color: var(--text-primary);
    font-weight: 600;
}

/* ═══ Top Bar ═══ */
.top-row {
    background-color: var(--bg-surface) !important;
    border-bottom: 1px solid var(--border);
    justify-content: space-between !important;
    height: 56px;
    padding: 0 1.5rem !important;
}

.top-row a, .top-row .btn-link {
    color: var(--text-primary) !important;
    text-decoration: none;
}

/* ═══ Sidebar ═══ */
.sidebar {
    background-color: var(--bg-surface) !important;
    background-image: none !important;
    /* Width is owned by the scoped layout CSS (MainLayout.razor.css):
       250px persistent sidebar at >=992px, 250px slide-in drawer below. */
    border-right: 1px solid var(--border);
}

.nav-scrollable {
    display: flex;
    flex-direction: column;
}

.nav-item a {
    color: var(--text-secondary) !important;
    border-radius: 8px;
    margin: 2px 8px;
    padding: 0.5rem 0.75rem;
    transition: var(--transition);
    display: flex;
    align-items: center;
    font-size: 0.875rem;
    font-weight: 500;
}

/* 🚨 A BRAND LEAK, and it only showed on team.help. This was a hardcoded
   rgba(255, 84, 50, 0.12) — MOTLogs orange #FF5432 at 12% — sitting behind a `color` that
   correctly followed var(--accent). So on every palette EXCEPT motlogs the active item painted
   the tenant's accent text on an orange wash: magenta on orange over a near-black sidebar reads
   as murky brown, which is what it looked like on damson.

   --accent-glow already exists and is defined for all ten palette/theme combinations (and is
   aliased to --accent-glow at the top of this file), so the tint now follows the palette by
   construction rather than by someone remembering to add a fifth override. Nothing here needs
   re-tuning when a palette is added. */
.nav-item a.active {
    background-color: var(--accent-glow) !important;
    color: var(--accent) !important;
}

.nav-item a:hover {
    background-color: var(--bg-elevated) !important;
    color: var(--text-primary) !important;
}

/* ═══ Buttons ═══ */
.btn-pill {
    border-radius: 8px;
    padding: 0.5rem 1.2rem;
    font-weight: 500;
    font-size: 0.8125rem;
    transition: var(--transition);
    border: none;
    cursor: pointer;
}

.btn-primary-mot {
    background-color: var(--accent);
    color: white;
}

.btn-primary-mot:hover {
    background-color: var(--accent-hover);
}

.btn-primary {
    background-color: var(--accent) !important;
    border-color: var(--accent) !important;
    color: white !important;
}

.btn-primary:hover {
    background-color: var(--accent-hover) !important;
    border-color: var(--accent-hover) !important;
}

/* ═══ Surface Card ═══ */
.glass-card {
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--border-radius);
    padding: 1.5rem;
    transition: var(--transition);
}

/* ── Page headers: the same lighter card every list screen already uses ──
   Call History, Recordings, Customers and the rest wrap their title in a
   .glass-card, so they sit on a raised surface; eleven other pages rendered a
   bare <div class="page-header"> with NO styling of its own, so the title sat
   flat on the page background and those screens read as a different, less
   finished product.

   Done here rather than by rewriting eleven headers: .page-header is already the
   shared marker, so one rule fixes every page that has it AND every page added
   later. Rewriting the markup would have converted the eleven that exist today
   and left the twelfth to be noticed by eye.

   Only the surface is borrowed, not the padding rhythm — these headers carry an
   <h1> where the card-wrapped ones carry an <h2>, so the same 1.5rem would read
   as too tight around the larger type. No :hover border shift either: a header
   is not a hoverable object, and glass-card's transition would make the whole
   band twitch as the pointer crossed it. */
.page-header {
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--border-radius);
    padding: 1.25rem 1.5rem;
    margin-bottom: 1.5rem;
}

/* ── The header bar: title left, controls right, identically on every page ──
   "It should be the same height from the top and the same distance from the
   right." That is a layout guarantee, so it lives here rather than in eleven
   pages' markup — align-items:center fixes the vertical offset against the
   title block, and the container's own padding-right (from .page-header above)
   fixes the horizontal one. A page that adds its own ms-auto or margin breaks
   both, which is how the drift started.

   flex-wrap so a narrow viewport drops the controls under the title instead of
   crushing the description into a column; the gap then becomes the row spacing,
   which is why it is set on the bar and not as a margin on either child. */
/* ── Line-card usage counters: icon + number when the card is narrow ──
   The four counters (📞 calls / 💬 msgs / 📱 WhatsApp / 🎥 meetings) are
   text-nowrap, which means a label that does not fit OVERFLOWS rather than
   wrapping — on a narrow card they ran into each other and clipped mid-word
   ("1 call" over "0 WhatsAp"). The icon already carries the meaning, so below a
   threshold the word is dropped and the number stays; the title tooltip on each
   counter still spells it out in full.

   🪤 A CONTAINER QUERY, NOT A MEDIA QUERY. These cards are three-up on a wide
   viewport, so the card can be narrow while the VIEWPORT is not — which is
   exactly the case in the report. Bootstrap's d-none/d-sm-inline utilities key
   off the viewport and would have left the widest screens broken while
   "fixing" phones that were never the problem.

   The @container fallback matters: where container queries are unsupported the
   inline-size condition never matches, so the label simply stays — the old
   behaviour, not a blank counter. */
.line-usage {
    container-type: inline-size;
    container-name: lineusage;
}

@container lineusage (max-width: 260px) {
    .line-usage-counts .usage-label {
        display: none;
    }
}

.page-header-bar {
    display: flex;
    justify-content: space-between;
    /* 🪤 flex-start, NOT center. Centring aligns the controls against the whole title
       BLOCK — title plus description — so on a page with a long description they sit
       level with the gap between the two lines rather than with the heading. Starting
       them puts their top edge on the heading's, which is what "in line with the main
       heading" means. */
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 1rem;
}

/* 🪤 flex-1 WITH A BASIS, not just min-width:0. Without a basis the description sets the
   block's width, and a long one (Missed Calls, Outbound) claims the whole row — which
   pushed the controls onto a second line UNDER the text, exactly the drift this bar was
   built to stop. The basis is the point below which wrapping is genuinely better than
   crushing both columns. */
.page-header-titles {
    flex: 1 1 320px;
    min-width: 0;
}

.page-header-title {
    margin-bottom: 0.25rem;
}

.page-header-sub {
    margin-bottom: 0;
    color: var(--text-secondary);
}

/* margin-left:auto rather than justify-content alone: with flex-wrap on, a wrapped
   row loses space-between's effect, and the controls would jump to the left edge
   on exactly the narrow screens where position matters most. */
/* Full width, below both columns — flex-basis:100% makes it take its own row even
   though it is a sibling of the title and controls, so it never squeezes either. */
/* :empty because the slot is filled with a CONDITIONAL — Home renders the fragment
   always and its @if decides whether anything is inside. Without this, flex-basis
   100% forces a wrap row for a div with nothing in it, adding a visible gap under
   the bar on exactly the page that is the dashboard's front door. */
.page-header-footnote:empty {
    display: none;
}

.page-header-footnote {
    flex-basis: 100%;
    margin-top: 0.25rem;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.page-header-controls {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-left: auto;
}

/* The LOWER HALF of the header: the page's own filters (search, status dropdown, checkboxes).

   flex-basis:100% for the same reason the footnote uses it — it is a sibling of the title and
   controls inside the same wrapping flex row, and a full basis is what makes it take its own line
   instead of competing with them for the top row. Without it a search field on Customers would
   claim width from the title block and push Refresh down, which is the drift this bar exists to
   stop.

   🪤 NOT margin-left:auto. The filters read LEFT, under the title they filter; only the top-row
   controls are pinned right. Giving both rows the same right-alignment made the search box hang off
   the right edge under Refresh, which read as a second toolbar rather than as part of the header. */
.page-header-filters {
    flex-basis: 100%;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 0.75rem;
}

/* Same :empty guard as the footnote, and for the same reason: a page can fill this slot with a
   fragment whose own @if renders nothing (an entitlement-gated filter), and flex-basis:100% would
   then reserve a blank row inside the header card. */
.page-header-filters:empty {
    display: none;
}

/* Inputs inside the filter row must not stretch to the full row. Bootstrap's .input-group is
   display:flex with width:100%, so an unconstrained search box swallows the whole width and the
   row stops looking like a filter strip and starts looking like a form. Capped, and allowed to
   shrink on a phone rather than forcing the header card to scroll sideways. */
.page-header-filters .input-group {
    width: auto;
    max-width: 26rem;
    flex: 1 1 16rem;
}

.page-header > :last-child {
    margin-bottom: 0;      /* the trailing <p> otherwise pads the card unevenly */
}

.glass-card:hover {
    border-color: var(--border-strong);
}

/* Bootstrap tables default --bs-table-bg to the light body colour and paint it
   onto every cell, which overrides the dark glass-card. Keep tables transparent
   so the card shows through, and inherit the theme text colour. */
.glass-card .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--text-primary);
    color: var(--text-primary);
}

/* Utility: paint a box with the theme surface colour. Several pages wrap content in
   `.card bg-surface` expecting this to exist — without it the Bootstrap .card falls back
   to its white --bs-card-bg and renders light in dark mode (e.g. the Outbound table card).
   Theme-aware via the --bg-surface variable (dark in default theme, white under data-theme=light). */
.bg-surface {
    background-color: var(--bg-surface) !important;
    border-color: var(--border) !important;
    color: var(--text-primary);
}

/* Tables inside a .bg-surface card: same treatment as .glass-card .table — keep cells
   transparent so the dark surface shows through instead of Bootstrap's light table bg. */
.bg-surface .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--text-primary);
    color: var(--text-primary);
}

/* ═══ Status Indicators ═══ */
.status-indicator {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    display: inline-block;
    margin-right: 8px;
    background-color: var(--text-muted);
}

.status-active {
    background-color: var(--success);
    box-shadow: 0 0 6px var(--success);
    animation: pulse 2s infinite;
}

@keyframes pulse {
    0%, 100% { opacity: 0.7; }
    50% { opacity: 1; }
}

/* ═══ Slot Card ═══ */
.slot-card {
    border-left: 3px solid var(--accent);
    margin-bottom: 0.75rem;
    padding: 1rem 1.25rem;
    background: var(--bg-elevated);
    border-radius: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* ═══ Tables ═══ */
/* Every table in this app carries .table-dark — it is how they get the app's surface instead of
   Bootstrap's white one, on BOTH themes. The name is historical; read it as "the app's table".
 *
 * 🚨 --bs-table-color IS THE ONE THAT MATTERS, and it was the one missing. Bootstrap declares
 *   .table-dark { --bs-table-color: #fff; ... }
 * and then colours the CELLS, not the table, with
 *   .table > :not(caption) > * > * { color: var(--bs-table-color-state,
 *                                             var(--bs-table-color-type,
 *                                             var(--bs-table-color))); }
 * so the `color: var(--text-primary)` below only ever reached the <table> element. Every <th> and
 * every <td> kept Bootstrap's hard-coded #fff. On the dark theme that is invisibly correct; on the
 * LIGHT theme it is white text on a white card — the Social videos table rendered as blank rows
 * with only the badges and .text-secondary spans (which set their own colour) still legible.
 *
 * So override the VARIABLES, not the property: the striped/active/hover variants too, or the row
 * under the pointer flashes back to white on light. Keep the plain `color` as well for anything
 * that inherits from the table element rather than a cell. */
.table-dark {
    --bs-table-color: var(--text-primary);
    --bs-table-bg: transparent;
    --bs-table-border-color: var(--border);
    --bs-table-hover-bg: var(--bg-elevated);
    --bs-table-hover-color: var(--text-primary);
    --bs-table-striped-color: var(--text-primary);
    --bs-table-active-color: var(--text-primary);
    color: var(--text-primary);
}

/* ═══ Form Controls (dark) ═══ */
.form-control, .form-select {
    background-color: var(--bg-elevated) !important;
    color: var(--text-primary) !important;
    border-color: var(--border-strong) !important;
}

.form-control:focus, .form-select:focus {
    border-color: var(--accent) !important;
    box-shadow: 0 0 0 2px rgba(255, 84, 50, 0.2) !important;
}

.form-text {
    color: var(--text-muted) !important;
}

.form-label {
    color: var(--text-secondary);
    font-weight: 500;
    font-size: 0.8125rem;
}

/* ═══ Alerts ═══ */
.alert-success {
    background-color: rgba(38, 176, 80, 0.12);
    border-color: rgba(38, 176, 80, 0.25);
    color: var(--success);
}

/* ═══ Text Utilities ═══ */
.text-muted {
    color: var(--text-secondary) !important;
}

/* ═══ Content Area ═══ */
.content {
    padding-top: 1.5rem;
}

/* ═══ Stat Icon ═══ */
.stat-icon {
    width: 48px;
    height: 48px;
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto;
}

/* ═══ Quick Action Links ═══ */
.action-link {
    display: block;
    padding: 0.75rem 1rem;
    background: var(--bg-elevated);
    border-radius: 8px;
    text-decoration: none;
    color: var(--text-primary);
    transition: var(--transition);
    margin-bottom: 0.5rem;
}

.action-link:hover {
    background: var(--bg-primary);
    color: var(--accent);
}

.action-link .text-muted {
    font-size: 0.8125rem;
}

/* ═══ Status Row ═══ */
.status-row {
    padding: 0.75rem 1rem;
    background: var(--bg-elevated);
    border-radius: 8px;
    border-left: 3px solid var(--success);
    margin-bottom: 0.5rem;
}

/* ═══ Loading Skeleton ═══ */
.skeleton {
    background: linear-gradient(90deg, var(--bg-elevated) 25%, var(--bg-surface) 50%, var(--bg-elevated) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
    border-radius: 6px;
}

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

/* ═══ Blazor Defaults ═══ */
#blazor-error-ui {
    background: var(--error);
    color: white;
    bottom: 0;
    box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.3);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

.loading-progress-text {
    font-family: "InterVariable","Inter",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
    color: var(--text-primary);
    font-weight: 600;
}

/* ═══ Scrollbar ═══ */
::-webkit-scrollbar {
    width: 6px;
}

::-webkit-scrollbar-track {
    background: var(--bg-primary);
}

::-webkit-scrollbar-thumb {
    background: var(--border-strong);
    border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--text-muted);
}

/* ═══════════════════════════════════════════════════════════════════════
   Responsive helpers (mobile + iPad portrait). All rules below are gated
   behind max-width media queries, so the >=992px desktop view is untouched.
   ═══════════════════════════════════════════════════════════════════════ */

/* Main content column is a flex item of .page (sidebar + main). By default a
   flex item has min-width:auto, so a wide descendant (e.g. the Flow diagram
   canvas) stops it shrinking to its track and pushes the WHOLE page off the
   right edge (clipped by html,body{overflow-x:hidden}). min-width:0 lets it
   shrink; inner overflow:auto/hidden boxes then scroll/clip their own content. */
main {
    min-width: 0;
}

/* ── Flow tab: conversation-flow diagram + Template-values pane ──
   The diagram has a computed fixed pixel width. Wrapping it in an
   overflow-x:auto box isn't enough on its own: a Bootstrap column is a flex
   item with the default min-width:auto, so it refuses to shrink below the
   diagram's intrinsic width, forcing the row wider than the viewport. With
   html,body{overflow-x:hidden} that overflow gets clipped, hiding the right
   edge of the Template-values pane (the "not showing 100%" symptom).
   Fix: the scroll box pans the diagram on touch; min-width:0 lets the left
   pane shrink so the box actually scrolls; the right pane is drag-resizable. */
.flow-scroll-wrap {
    position: relative;
}

.flow-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;   /* momentum panning on iPad */
    max-width: 100%;
    /* Native horizontal scrollbar is hidden — we render our own always-visible
       one (.flow-hbar) because iPad/macOS overlay scrollbars vanish when idle,
       so users couldn't tell the diagram pans. */
    scrollbar-width: none;               /* Firefox */
}

.flow-scroll::-webkit-scrollbar {        /* WebKit/Blink */
    height: 0;
    width: 0;
}

/* Custom horizontal scrollbar. Sticks to the bottom of the viewport while the
   (tall) diagram is in view, so it's reachable without scrolling to the very
   bottom. Shown only when the diagram is wider than its pane (JS toggles it). */
.flow-hbar {
    display: none;                       /* JS sets to block when scrollable */
    position: sticky;
    bottom: 8px;
    left: 0;
    height: 12px;
    margin-top: 6px;
    border-radius: 6px;
    background: var(--bg-elevated, rgba(255, 255, 255, 0.06));
    z-index: 3;
    touch-action: none;
}

.flow-hbar-thumb {
    position: absolute;
    top: 1px;
    left: 0;
    height: 10px;
    width: 40px;
    border-radius: 5px;
    background: var(--border-strong, #6a6a6a);
    cursor: grab;
}

.flow-hbar-thumb:hover,
.flow-hbar-thumb:active {
    background: var(--accent, #ff6a00);
}

/* ── Flow panes: ALWAYS side-by-side (never stack/wrap), at every width ──
   Two panes + a drag handle in one nowrap flex row that can NEVER exceed the
   viewport: overflow:hidden clips at the row box, zeroed gutters keep the box
   flush to the container, and each pane has a shrink-friendly min-width so its
   content scrolls internally instead of pushing the row wide. The right pane's
   width is the CSS var --flow-side (set live by flow-splitter.js / the header
   button); the diagram pane flexes to fill the remainder. No media query — this
   layout applies on phones too, so the Template-values pane never drops onto a
   second line. */
.flow-row {
    display: flex;
    flex-wrap: nowrap;
    align-items: stretch;
    overflow: hidden;
    margin-left: 0;
    margin-right: 0;
}

.flow-row > .flow-main-col,
.flow-row > .flow-side-col {
    padding-left: 0;
    padding-right: 0;
}

/* Diagram pane — fills whatever the values pane leaves; min-width:0 lets
   .flow-scroll pan the wide canvas instead of blowing the row out. */
.flow-main-col {
    flex: 1 1 0;
    min-width: 0;
}

/* Values pane (Template values / Node) — width driven by --flow-side. The
   min-width floor is the smaller of 260px and 42% so it stays readable on big
   screens yet can still shrink (never forcing the row wider than the viewport)
   on a phone. */
.flow-side-col {
    flex: 0 0 var(--flow-side, 36%);
    min-width: min(260px, 42%);
    max-width: none;
    overflow: auto;
}

/* The grab handle between the panes. */
.flow-splitter {
    flex: 0 0 14px;
    align-self: stretch;
    position: relative;
    cursor: col-resize;
    touch-action: none;            /* let JS own the drag gesture on touch */
    background: transparent;
}

.flow-splitter::before {           /* the visible vertical line */
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    transform: translateX(-50%);
    background: var(--border, #3a3a3a);
    border-radius: 2px;
}

.flow-splitter::after {            /* grip dots, centred */
    content: "⋮";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: var(--text-muted, #8a8a8a);
    font-size: 18px;
    line-height: 1;
}

.flow-splitter:hover::before,
.flow-splitter:active::before {
    background: var(--accent, #ff6a00);
    width: 3px;
}

/* ── Phones (< 768px): STACK the two Flow panes vertically ──
   The always-side-by-side rule above squeezes both panes into slivers on a phone
   — the Template-values pane and the diagram's "Pick a flow template…" text wrap
   one word per line. Below 768px, stack them: the diagram (full width; its own
   .flow-scroll pans the wide canvas horizontally) sits ABOVE the Template-values
   pane (full width), so everything is readable. The drag splitter is meaningless
   when stacked, so hide it. Breakpoint is 992px (the app's compact-mode cutoff, matching
   the sidebar drawer / tab-scroll / Actions overflow) — some phones report a CSS viewport
   of 768–992px (Samsung screen-zoom / large & foldable devices), so a 768px cutoff missed
   them. iPad landscape (>=992px) and desktop keep the side-by-side view. */
@media (max-width: 991.98px) {
    .flow-row {
        flex-direction: column;
        overflow: visible;
    }
    .flow-row > .flow-main-col,
    .flow-row > .flow-side-col {
        flex: 1 1 auto;
        width: 100%;
        min-width: 0;
        max-width: none;
    }
    .flow-side-col {
        margin-top: 0.75rem;
    }
    .flow-splitter {
        display: none;
    }
}

/* The mobile hamburger button — hidden at >=992px via Bootstrap's d-lg-none. */
.sidebar-toggle {
    line-height: 1;
    padding: 0.2rem 0.55rem;
    font-size: 1.1rem;
}

/* Filter bars: let inline filter rows wrap instead of overflowing on phones.
   Add class="filter-bar" alongside the existing d-flex utilities. */
.filter-bar {
    flex-wrap: wrap;
}

/* ── Card-stacked tables (phones + iPad portrait, < 992px) ──
   Add class="table-stack" to a <table> and data-label="Column" to each <td>.
   Below 992px the table collapses into one card per row with inline labels;
   at >=992px it renders as a normal table (no change). The breakpoint matches
   the sidebar drawer cutoff so iPad portrait (768px) — which previously fell
   through to a cramped 9-column horizontal-scroll table that hid the action
   buttons — now gets the readable card view. iPad landscape (1024px) stays a
   normal table. */
@media (max-width: 991.98px) {
    .table-stack thead {
        /* visually hide the header row — labels come from data-label */
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    .table-stack,
    .table-stack tbody,
    .table-stack tr,
    .table-stack td {
        display: block;
        width: 100%;
    }

    .table-stack tr {
        background: var(--bg-surface);
        border: 1px solid var(--border);
        border-radius: var(--border-radius);
        margin-bottom: 0.75rem;
        padding: 0.25rem 0.75rem;
    }

    .table-stack td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
        border: 0;
        border-bottom: 1px solid var(--border);
        padding: 0.5rem 0;
        text-align: right;
        /* override any inline fixed widths / nowrap from desktop markup */
        width: auto !important;
        white-space: normal !important;
    }

    .table-stack tr td:last-child {
        border-bottom: 0;
    }

    .table-stack td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--text-secondary);
        text-align: left;
        margin-right: auto;
        white-space: nowrap;
    }

    /* cells with no data-label (e.g. an actions row) span full width */
    .table-stack td:not([data-label]) {
        justify-content: flex-end;
    }
}

/* =====================================================================
 * team.help palette axis (plan §3.3b) — PURELY ADDITIVE, gated behind
 * [data-palette]. MOTLogs sets NO data-palette attribute, ever, so none of
 * the blocks below can match a MOTLogs surface — the :root{…} and
 * html[data-theme="light"]{…} blocks at the top of this file remain the sole
 * source of truth for MOTLogs and are intentionally left byte-untouched.
 *
 * The neutral skeleton (bg, surfaces, borders, radii, type, status colours)
 * is DELIBERATELY inherited unchanged from :root above — identical to
 * MOTLogs — so the palette axis swaps ONLY the accent triplet
 * (--ml-accent / --ml-accent-hover / --ml-accent-glow, which Agent maps to
 * --accent / --accent-hover / --accent-glow) plus the brand-level WCAG fixes
 * below. Glow = the accent as rgba at 0.15 (dark) / 0.10 (light).
 * ===================================================================== */

/* Brand-level WCAG fixes — apply to ALL four team.help palettes (any
   data-palette value matches); MOTLogs never matches. */
:root[data-palette] {
    --ml-text-muted:  #949494;   /* team.help dark muted (MOTLogs keeps #707070) */
    --ml-reading:     #D6D6D6;   /* long-form dark reading text */
}
html[data-theme="light"][data-palette] {
    --ml-text-muted:  #6B6B6B;
}

/* Primary-button ink. Agent hard-codes white ink on .btn-primary / .btn-primary-mot
   (there is no ink token to remap), so the team.help ink fix is applied by
   selector, still gated behind [data-palette] so MOTLogs is untouched: dark
   buttons take #121212 ink, light buttons keep #FFFFFF. */
:root[data-palette] .btn-primary,
:root[data-palette] .btn-primary-mot {
    color: #121212 !important;
}
html[data-theme="light"][data-palette] .btn-primary,
html[data-theme="light"][data-palette] .btn-primary-mot {
    color: #FFFFFF !important;
}

/* Per-palette accent triplets — override ONLY the accent trio. */
/* harbor */
:root[data-palette="harbor"] {
    --ml-accent:       #4C8DFF;
    --ml-accent-hover: #7AAEFF;
    --ml-accent-glow:  rgba(76, 141, 255, 0.15);
}
html[data-theme="light"][data-palette="harbor"] {
    --ml-accent:       #1D4ED8;
    --ml-accent-hover: #1E40AF;
    --ml-accent-glow:  rgba(29, 78, 216, 0.10);
}
/* evergreen */
:root[data-palette="evergreen"] {
    --ml-accent:       #14B8A6;
    --ml-accent-hover: #2DD4BF;
    --ml-accent-glow:  rgba(20, 184, 166, 0.15);
}
html[data-theme="light"][data-palette="evergreen"] {
    --ml-accent:       #0F766E;
    --ml-accent-hover: #115E59;
    --ml-accent-glow:  rgba(15, 118, 110, 0.10);
}
/* indigo */
:root[data-palette="indigo"] {
    --ml-accent:       #818CF8;
    --ml-accent-hover: #A5B4FC;
    --ml-accent-glow:  rgba(129, 140, 248, 0.15);
}
html[data-theme="light"][data-palette="indigo"] {
    --ml-accent:       #4F46E5;
    --ml-accent-hover: #4338CA;
    --ml-accent-glow:  rgba(79, 70, 229, 0.10);
}
/* damson */
:root[data-palette="damson"] {
    --ml-accent:       #E879F9;
    --ml-accent-hover: #F0ABFC;
    --ml-accent-glow:  rgba(232, 121, 249, 0.15);
}
html[data-theme="light"][data-palette="damson"] {
    --ml-accent:       #86198F;
    --ml-accent-hover: #701A75;
    --ml-accent-glow:  rgba(134, 25, 143, 0.10);
}

/* ═══════════════════════════════════════════════════════════════════════
   Agent config screen — mobile responsiveness pass (Pages/Agent.razor, items
   2–6). Every rule below is gated behind a max-width media query (or lives on a
   class that only renders < 992px via Bootstrap's d-lg-none), so the >=992px
   desktop view is byte-unchanged. This is the SHARED UI (MOTLogs uses it too);
   the same mobile gates apply on both surfaces. Class hooks added in the .razor:
   .tabbar-sticky, .tab-scroll, .action-overflow, .line-card, .line-usage.
   Follows the .flow-row / .table-stack conventions above.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Item 2: section-tab strip → single horizontal-scroll row (< 992px) ──
   Below the sidebar-drawer cutoff the seven section tabs (Agent · Phone · Hours ·
   Work Items · Knowledge · Flow · Web Widget) must NOT wrap into ragged multi-line
   rows: force one nowrap row that pans horizontally on touch. Bootstrap's
   flex-md-wrap (>=768px, !important) is overridden here with !important so the
   768–991 band scrolls too; below 768 the markup's flex-nowrap already applies.
   Desktop (>=992px) keeps the current wrapping layout — this block never matches. */
@media (max-width: 991.98px) {
    .tab-scroll {
        flex-wrap: nowrap !important;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;   /* momentum panning on touch */
        scrollbar-width: none;               /* Firefox — thin/hidden bar is fine */
    }
    .tab-scroll::-webkit-scrollbar {          /* WebKit/Blink */
        height: 0;
    }

    /* The nowrap above stops the STRIP wrapping, but not the LABEL inside each tab:
       "Hours & Routing" was breaking across three lines, which stretched the whole bar
       and pushed Actions onto its own row. Keep each label on one line and let the strip
       pan instead — that is the behaviour the nowrap was reaching for. */
    .tab-scroll .nav-link {
        white-space: nowrap;
    }

    /* ── Item 4: sticky compact header (< 992px) ──
       The line-selector + tab-strip row is a direct child of .container-fluid, so
       its sticky containing block spans the whole form: the bar pins to the top
       while the tab content scrolls under it. The inner glass-card's background is
       the SOLID --bg-surface token (opaque despite the name) so nothing bleeds
       through; a bottom border separates it and z-index keeps it above content.
       Vertical padding is trimmed so the pinned bar stays compact. */
    .tabbar-sticky {
        position: sticky;
        top: 0;
        z-index: 1020;
    }
    .tabbar-sticky .glass-card {
        background: var(--bg-surface);
        border-bottom: 1px solid var(--border);
        padding-top: 0.4rem !important;
        padding-bottom: 0.4rem !important;
    }
}

/* ── Item 3: action buttons → no-JS overflow menu (< 992px) ──
   The desktop action group (Usage / Test line / Close + tier badge) is hidden by
   d-none d-lg-flex; this <details class="action-overflow d-lg-none"> is shown only
   below 992px and its items invoke the SAME @onclick handlers (SetActiveTab
   ("usage"), RunPreflightAsync, selectedLine = null). d-lg-none already sets
   display:none at >=992px, so the (unconditional) styling below cannot affect the
   desktop view. */
.action-overflow {
    position: relative;
}
.action-overflow > summary {
    list-style: none;                         /* hide the default disclosure triangle */
    cursor: pointer;
}
.action-overflow > summary::-webkit-details-marker {
    display: none;
}
.action-overflow-menu {
    position: absolute;
    /* Anchor LEFT, not right. `right: 0` pins the menu's right edge to the Actions
       summary — and on a narrow screen that summary wraps onto its own row at the far
       left, so the menu grew leftwards and ran off the viewport: the items rendered
       clipped ("…sage", "…AQ", "…est line", "…ose"). Anchoring left opens it inward from
       where the button actually sits, and the max-width keeps it on screen at the other
       end rather than trading a left overflow for a right one. */
    left: 0;
    right: auto;
    top: calc(100% + 4px);
    z-index: 1030;                            /* above the sticky bar's own context */
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 160px;
    max-width: min(280px, calc(100vw - 32px));
    padding: 8px;
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--border-radius);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.action-overflow-menu .btn {
    width: 100%;
    text-align: left;
}

/* ── Item 5: touch targets (< 768px) ──
   Bump the small tap targets that live in the sticky tab-bar (section tabs, the
   Actions summary + its menu buttons, and any Close/× icon buttons) to a ~44px
   minimum so they're comfortable on a phone. Scoped to the two Agent-screen
   containers (.tabbar-sticky, .action-overflow) so no other control changes;
   centred flex keeps existing alignment. */
@media (max-width: 991.98px) {
    .tab-scroll .nav-link,
    .tabbar-sticky .btn,
    .tabbar-sticky .btn-close,
    .action-overflow > summary,
    .action-overflow-menu .btn {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .action-overflow-menu .btn {
        justify-content: flex-start;          /* left-align the stacked menu items */
    }
}

/* ── Item 6: subscription line cards — tighter on mobile (< 768px) ──
   The cards already drop to one column below md (col-md-6). Trim the generous
   1.5rem glass-card padding and collapse the gaps in the USAGE / calls / msgs /
   £cap / renews block (.line-usage) so each card is shorter and less scrolly on a
   phone. iPad portrait+ (>=768px) and desktop are untouched. */
@media (max-width: 991.98px) {
    .line-card {
        padding: 0.85rem;
    }
    .line-usage .small {
        margin-bottom: 0.15rem !important;
    }
    .line-usage .mb-2 {
        margin-bottom: 0.35rem !important;
    }
    .line-usage .progress {
        margin-top: 0.35rem !important;
    }
}

/* ── The action bar at the foot of a subscription line card ──
   ONE rule for all three actions (Set up, Interview, Directory default), because they are
   three different ELEMENTS — a .btn, a .btn, and a <span>/<label> that used to be a .badge —
   and each carried its own inline font-size (11px, 10px, 10px) and its own padding. Three
   sizes in one row read as three unrelated chips at three heights, and at 10px on a phone
   none of them was a comfortable tap target.

   Sized against the 44px touch guidance rather than the old 18px: 36px min-height, real
   horizontal padding, one font size. inline-flex + centre is what actually aligns the text
   across a <button> and a <label> — a bare .badge sits on the text baseline, a .btn on its
   box, which is why the pill always looked a pixel or two adrift of its neighbours.

   Scoped under .line-card and using the same one-class weight as Bootstrap's .btn so it wins
   on source order (app.css loads after bootstrap) without !important. */
.line-card .line-card-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 36px;
    padding: 0.4rem 0.95rem;
    font-size: 0.8125rem;
    font-weight: 500;
    line-height: 1.2;
    border-radius: 8px;
    white-space: nowrap;
    margin: 0;                       /* the row owns the spacing, via gap */
    transition: var(--transition);
}

/* The star pill states a fact ("this IS the directory default") — no hover affordance and no
   pointer, unlike its clickable "make this one" twin, which keeps the .btn-like hover. */
.line-card .line-card-action-static {
    cursor: default;
}

@media (max-width: 575.98px) {
    /* Narrow phones: three pills at full width wrap to three rows and eat the card. Let them
       shrink a little rather than stack. */
    .line-card .line-card-action {
        padding: 0.35rem 0.7rem;
        font-size: 0.75rem;
    }
}

/* Item 12: Support Desk tier badge — a distinct coloured pill. Previously bg-dark, which
   was near-invisible on the dark surface. Applied everywhere GetTierBadgeClass renders the
   business_support tier (line cards + the selected-line header badge). */
.badge-support-desk {
    background-color: #8b5cf6 !important;
    color: #fff !important;
}

/* Reusable "this view is filtered" pill — accent-tinted rather than accent-filled, because it
   states a condition the user put the page into and should not shout louder than the data it is
   describing. Text is --accent (which every palette guarantees against the surface) over the
   matching --accent-glow wash, so it tracks brand, palette and theme with no per-brand rule. */
.bg-accent-subtle {
    background-color: var(--accent-glow);
    color: var(--accent);
}

/* Reusable "selected row/card" border in the brand accent colour, so every clickable grid
   highlights its selected row with the defined colour scheme (not Bootstrap's blue border-primary).
   Pair with Bootstrap's .border (which supplies width+style): class="border border-accent". */
.border-accent {
    border-color: var(--accent) !important;
}

/* Internal wordmark (NavMenu / top bar): colour the ".help" accent span so the in-app logo
   matches the marketing standard (team in the foreground colour, .help in the brand accent).
   Without this the app shell had no rule for .th-wordmark .accent, so ".help" inherited the
   white nav text and the whole lockup rendered all-white.

   The "team" half needs its own rule for the SAME reason, one theme along. The lockup sits in
   `.top-row.navbar.navbar-dark`, and Bootstrap's `.navbar-dark .navbar-brand` sets a near-white
   colour ON THE ELEMENT — so the wordmark inherits white no matter what the theme says. In dark
   mode that happens to be right and hid the bug; in light mode the top bar is --bg-surface, i.e.
   white, and "team" was white text on it. Invisible, with ".help" left floating on its own,
   because the accent rule above was the only thing overriding the navbar colour.

   Bound to --text-primary rather than a literal so it tracks both themes (#1A1A1A light,
   #FFFFFF dark) and every palette. The rule below `.top-row a` deliberately mirrors: same token,
   same reason, and this is the non-anchor case that one does not reach. */
.th-wordmark {
    color: var(--text-primary);
}

.th-wordmark .accent {
    color: var(--accent);
}

/* ═══════════════════════════════════════════════════════════════════════════════════════
   DASHBOARD V2 — CHART TOKENS AND CHART MARKS
   Token names frozen by Documents/DASHBOARD_V2_CONTRACT.md §6. Components reference these
   through var(--chart-*) only; there is no raw hex in any .razor file.

   THE ACCENT IS NOT A SERIES COLOUR. --accent / --ml-accent is reserved for interactive and
   brand elements, and no series value below is derived from it. That is not a stylistic
   preference — it is what stops a chart bar reading as a button.

   Everything here was produced by RUNNING the palette validator (six computable checks:
   lightness band, chroma floor, adjacent-pair CVD separation under protan/deutan, the
   normal-vision floor, contrast vs the card surface), not by eye. Card surface is
   --bg-surface: #1E1E1E dark, #FFFFFF light. Results:

     dark  #10A093,#D95926,#3987E5,#D55181 → PASS all six
           (worst adjacent CVD ΔE 13.9 deutan; worst adjacent normal ΔE 26.5)
     light #0F8A78,#C2571F,#2A78D6,#C13F70 → PASS all six
           (worst adjacent CVD ΔE 12.1 protan; worst adjacent normal ΔE 23.5)

   SLOT ORDER IS THE COLOUR-VISION-DEFICIENCY SAFETY MECHANISM. The CVD check tests ADJACENT
   pairs, so re-ordering these four values changes which pairs get checked. Do not reorder,
   substitute or add a fifth slot without re-running the validator on the new order.

   ACCENT-COLLISION AUDIT (measured per brand and theme; OKLab ΔE ×100, normal vision and
   worst of protan/deutan; a hue is "colliding" below 15 normal or 8 CVD):

     motlogs   dark  #FF5432 → s1 31.6/13.8  s2  7.2/3.0 ✗  s3 35.4/25.4  s4 12.6/12.8 ✗
     motlogs   light #CC3415 → s1 28.3/12.4  s2  5.9/2.3 ✗  s3 33.2/27.8  s4 10.2/10.0 ✗
     harbor    dark  #4C8DFF → s1 19.0/17.3  s2 33.5/30.0  s3  4.5/3.8 ✗  s4 27.3/19.6
     harbor    light #1D4ED8 → s1 24.6/21.9  s2 36.1/29.4  s3 10.7/7.9 ✗  s4 30.1/17.6
     evergreen dark  #14B8A6 → s1  7.0/6.8 ✗ s2 29.2/15.0  s3 18.9/18.1  s4 30.5/7.6 ✗
     evergreen light #0F766E → s1  6.1/5.9 ✗ s2 23.8/10.0  s3 16.7/16.2  s4 26.2/6.1 ✗
     indigo    dark  #818CF8 → s1 20.1/14.9  s2 29.7/28.2  s3  8.4/4.0 ✗  s4 22.8/18.2
     indigo    light #4F46E5 → s1 27.0/22.1  s2 35.2/31.0  s3 11.8/6.8 ✗  s4 27.7/19.4
     damson    dark  #E879F9 → s1 31.8/14.8  s2 27.1/25.9  s3 24.2/4.8 ✗  s4 18.0/17.4
     damson    light #86198F → s1 30.5/15.3  s2 25.9/25.7  s3 23.7/13.0  s4 16.2/13.8

   Every brand collides with at least one hue, and reordering cannot remove a hue from the
   set — so the rule applied here is: THE COLLIDING HUE MAY NOT OCCUPY A SLOT THE DASHBOARD
   ACTUALLY PAINTS. The orderings below were enumerated against slots 1 AND 3, because v1
   painted both (CallsPerDayChart + OutcomeBucketsChart on series-1, CallsByHourChart on
   series-3). SINCE 2026-08-01 THE DASHBOARD PAINTS SLOT 1 ONLY: all three charts are
   single-series and every one of them measures calls, so two hues for one measure was a
   distinction the data does not have. The orderings are UNCHANGED and stay correct — keeping
   the colliding hue out of slots 1 and 3 also keeps it out of slot 1. Re-enumerate only if a
   chart ever needs a slot other than 1 (then 3, then 4 — never 2, see the caveat below).
   Those orderings were enumerated exhaustively, not chosen:

     motlogs   (teal, orange, blue, pink)  — exactly ONE valid+safe ordering exists and it is
                                             the frozen base order. Its colliding hues
                                             (orange, pink) already sit in the unpainted
                                             slots 2 and 4. No override needed, and the
                                             contract's frozen values stand unchanged.
     harbor    (pink, blue, orange, teal)  — dark CVD 13.9 / normal 26.5; light 12.1 / 23.5
     indigo    (pink, blue, orange, teal)  — same set, same measurements
     damson    (pink, blue, orange, teal)  — dark only needs it (light has no collision), but
                                             both themes use one ordering so a light/dark
                                             toggle never repaints a series
     evergreen (orange, teal, blue, pink)  — dark CVD 13.9 / normal 16.2; light 12.1 / 17.0
                                             (its accent collides with BOTH teal and pink, so
                                             this is the only ordering that clears slots 1+3)

   CAVEAT for whoever adds a third chart series: on every [data-palette] brand, --chart-series-2
   deliberately holds that brand's accent-colliding hue. Painting a mark with series-2 on
   team.help makes it look interactive. Add series in the order 1, 3, 4 — and if a fourth is
   genuinely needed, re-run the validator rather than reaching for series-2.

   --chart-trend-up / --chart-trend-down are STATUS colours, never "series 5". They are a
   green/red pair and green/red is the classic CVD failure (measured on this exact pair:
   ΔE 3.5 deutan on dark, 4.6 on light, against an 8 floor), which is why TrendDelta always
   ships an arrow glyph and a signed number alongside the colour.
   ═══════════════════════════════════════════════════════════════════════════════════════ */

/* Base = dark, matching the :root convention at the top of this file. */
:root {
    --chart-series-1: #10A093;
    --chart-series-2: #D95926;
    --chart-series-3: #3987E5;
    --chart-series-4: #D55181;
    --chart-grid: rgba(255, 255, 255, 0.08);
    --chart-axis-text: rgba(255, 255, 255, 0.55);
    --chart-trend-up: #2E9E5B;
    /* Was #D64545 = 3.81:1 on the #1E1E1E card surface — under AA for .trend-delta's
       0.78rem/600 text, which is normal-size text, not large. #E05A5A measures 4.59:1.
       Its light twin #B93838 was already 5.70:1 and is unchanged. */
    --chart-trend-down: #E05A5A;
    /* Contextual shading (weekend columns, opening hours). Encodes no value. */
    --chart-band: rgba(255, 255, 255, 0.04);
    /* Pointer affordance. MUST NOT equal --chart-band: a hovered weekday that fills with the
       weekend tint reads as a weekend, destroying the only contextual encoding the chart has. */
    --chart-hover: rgba(255, 255, 255, 0.09);
}

/* The boot script in index.html always stamps an explicit data-theme before first paint, so
   this media query only decides the no-JS case. The html[data-theme] blocks below are more
   specific AND later, so an explicit choice wins over the OS preference in both directions. */
@media (prefers-color-scheme: light) {
    :root {
        --chart-series-1: #0F8A78;
        --chart-series-2: #C2571F;
        --chart-series-3: #2A78D6;
        --chart-series-4: #C13F70;
        --chart-grid: rgba(0, 0, 0, 0.08);
        --chart-axis-text: rgba(0, 0, 0, 0.55);
        --chart-trend-up: #1D7A43;
        --chart-trend-down: #B93838;
        --chart-band: rgba(0, 0, 0, 0.04);
        --chart-hover: rgba(0, 0, 0, 0.07);
    }
}

html[data-theme="dark"] {
    --chart-series-1: #10A093;
    --chart-series-2: #D95926;
    --chart-series-3: #3987E5;
    --chart-series-4: #D55181;
    --chart-grid: rgba(255, 255, 255, 0.08);
    --chart-axis-text: rgba(255, 255, 255, 0.55);
    --chart-trend-up: #2E9E5B;
    --chart-trend-down: #E05A5A;
    --chart-band: rgba(255, 255, 255, 0.04);
    --chart-hover: rgba(255, 255, 255, 0.09);
}

html[data-theme="light"] {
    --chart-series-1: #0F8A78;
    --chart-series-2: #C2571F;
    --chart-series-3: #2A78D6;
    --chart-series-4: #C13F70;
    --chart-grid: rgba(0, 0, 0, 0.08);
    --chart-axis-text: rgba(0, 0, 0, 0.55);
    --chart-trend-up: #1D7A43;
    --chart-trend-down: #B93838;
    --chart-band: rgba(0, 0, 0, 0.04);
    --chart-hover: rgba(0, 0, 0, 0.07);
}

/* ── Per-brand series ORDERING, gated behind [data-palette] ──
   Same four validated hues, re-ordered so the brand's accent-colliding hue lands in a slot
   the dashboard does not paint. MOTLogs sets NO data-palette attribute, ever, so none of
   these blocks can match a MOTLogs surface and the values above remain its sole source of
   truth. Structure mirrors the accent-triplet blocks above exactly, including the reason the
   light selector carries both attributes: :root[data-palette="x"] (0,2,0) would otherwise
   outrank html[data-theme="light"] (0,1,1) and paint dark values on a light page.
   Grid / axis / trend / band tokens are inherited unchanged — only the four series slots move. */

/* harbor · indigo · damson — accent collides with the blue; blue moves to slot 2. */
:root[data-palette="harbor"],
:root[data-palette="indigo"],
:root[data-palette="damson"] {
    /* DAMSON SERIES-1 RETUNE — ATTEMPTED 2026-08-07, REJECTED BY MEASUREMENT. It reads as a near
       miss for the accent (#E879F9) and the ask was to harmonise it. Every candidate closer in hue
       to the accent regresses one of the six checks this palette was built on: #B84A9A, the
       obvious pick, drops contrast against the #1E1E1E card from 4.23:1 to 3.56:1 while improving
       accent separation by 0.1. A search over the pink/magenta/violet space for anything that
       regresses NOTHING (contrast vs card, accent-collision floor, adjacent 1-2 CVD) returns
       nothing nearer than ~39° of hue — which is where #D55181 already sits.
       Measured: #D55181 vs accent = 18.0 normal / 18.3 worst-CVD, contrast 4.23:1.
       So this value STAYS. Making slot 1 read as deliberate rather than nearly-matching needs a
       different HUE in slot 1, which means re-enumerating the ordering and re-running the
       validator — not a value tweak. Do not "improve" this hex without re-measuring. */
    --chart-series-1: #D55181;
    --chart-series-2: #3987E5;
    --chart-series-3: #D95926;
    --chart-series-4: #10A093;
}
html[data-theme="light"][data-palette="harbor"],
html[data-theme="light"][data-palette="indigo"],
html[data-theme="light"][data-palette="damson"] {
    --chart-series-1: #C13F70;
    --chart-series-2: #2A78D6;
    --chart-series-3: #C2571F;
    --chart-series-4: #0F8A78;
}

/* evergreen — accent collides with the teal (and, for CVD readers, the pink); both move off
   the painted slots. */
:root[data-palette="evergreen"] {
    --chart-series-1: #D95926;
    --chart-series-2: #10A093;
    --chart-series-3: #3987E5;
    --chart-series-4: #D55181;
}
html[data-theme="light"][data-palette="evergreen"] {
    --chart-series-1: #C2571F;
    --chart-series-2: #0F8A78;
    --chart-series-3: #2A78D6;
    --chart-series-4: #C13F70;
}

/* ═══ Slot 1 becomes PER-PALETTE, harmonised with that palette's accent ═══════════════════
   The orderings above solve "keep the colliding hue out of slot 1". They do not solve the
   thing a user actually sees: harbor, indigo and damson all landed on the SAME rose pink, so
   on a blue dashboard the bars looked like they belonged to a different product. The four
   frozen hues cannot fix that by re-ordering — for a blue or indigo accent, the only members
   of the set that clear the separation floor are the pink (current) and the orange, so every
   re-ordering is either no change or turns three palettes orange.

   So slot 1 alone gets a per-palette value, chosen to sit closer to the accent's own hue while
   holding every constraint the set was built on. Slots 2-4 are untouched: the colliding hue
   stays parked in slot 2 on every brand.

   HOW THESE WERE PICKED — reproduce before changing one. Exhaustive sRGB search, keeping only
   colours that clear ALL of:
     • WCAG contrast >= 4.2:1 against the card the mark sits on (#1E1E1E dark, #FFFFFF light)
     • CIEDE2000 >= 16.5 from that palette's accent in NORMAL vision AND under the worst of
       deutan / protan / tritan simulation. 16.5 is not arbitrary: it is what damson's incumbent
       #D55181/#C13F70 measured, i.e. the tightest pair the system already shipped.
     • L and C inside the band of the four frozen hues (L 52-60 dark / 46-52 light, C 45-70), so
       a new value reads as a sibling of the set rather than a neon.
   Then: minimise hue distance to the accent. Light twins are additionally hue-matched to their
   dark partner (<= 12 degrees) so a theme toggle never repaints the series a different colour.

     harbor    #C65A9F / #B04E7A   contrast 4.26 / 4.98   dE 34.2 / 30.6   worst-CVD 16.6 / 16.6
     indigo    #CF51A2 / #AE4A7A   contrast 4.25 / 5.17   dE 28.0 / 27.3   worst-CVD 16.5 / 16.6
     damson    #E73C93 / #C05284   contrast 4.34 / 4.39   dE 19.0 / 20.7   worst-CVD 18.3 / 17.3
     evergreen #5D903C / #6A8232   contrast 4.38 / 4.33   dE 24.1 / 24.7   worst-CVD 16.6 / 16.6

   NOTE ON DAMSON, which the 2026-08-07 attempt above declared unimprovable. That search held
   lightness fixed and looked only for a nearer HUE; it correctly found nothing. Letting L move
   one point (53.6 -> 54.4) does find something: #E73C93 is 8 degrees closer to the accent than
   #D55181 AND measures BETTER on the check that matters most — worst-case CVD separation rises
   17.6 -> 18.3, with contrast up 4.23 -> 4.34. The earlier note stands as written; its
   conclusion just did not survive widening the search by one axis.

   The accent-collision rule itself is UNCHANGED and still the point: none of these is the
   accent, or near enough to it that a bar could be mistaken for something clickable.
   ═══════════════════════════════════════════════════════════════════════════════════════ */
:root[data-palette="harbor"]    { --chart-series-1: #C65A9F; }
:root[data-palette="indigo"]    { --chart-series-1: #CF51A2; }
:root[data-palette="damson"]    { --chart-series-1: #E73C93; }
:root[data-palette="evergreen"] { --chart-series-1: #5D903C; }

html[data-theme="light"][data-palette="harbor"]    { --chart-series-1: #B04E7A; }
html[data-theme="light"][data-palette="indigo"]    { --chart-series-1: #AE4A7A; }
html[data-theme="light"][data-palette="damson"]    { --chart-series-1: #C05284; }
html[data-theme="light"][data-palette="evergreen"] { --chart-series-1: #6A8232; }

/* ── Chart marks ──
   Components set --chart-series inline from whichever slot token they were given, so every
   mark below paints through one indirection and never a literal colour. */

.chart-block {
    width: 100%;
}

/* ── AXIS LABELS ARE HTML, NOT SVG <text> ──
   The plot is a fixed 720-unit coordinate space rendered with width:100%, so ANYTHING inside
   the SVG scales with the container. A 10px user-unit tick label painted at 3.3 CSS px on a
   390px phone (238px of card content -> scale 0.33) and 6.4px on a 1366px laptop at col-lg-6
   carries no information at all. OutcomeBucketsChart already proved the fix in this codebase
   by keeping its row labels in HTML; the two charts that actually have axes now do the same.

   Layout: a two-column grid puts a fixed-width gutter beside the plot for the y ticks, and a
   relatively-positioned strip under it carries the x ticks. Both are positioned by PERCENTAGE
   of the plot box, which is exactly what the SVG's own viewBox mapping is, so the labels track
   the marks at every width while their type stays at a real 0.7rem. */
/* ── CHART HEIGHT IS FIXED IN PIXELS, NOT DERIVED FROM THE VIEWBOX ──
   The SVG is a 720x180 coordinate space drawn at width:100%. With height:auto the browser
   honours that 4:1 intrinsic ratio, so the rendered HEIGHT is a function of how wide the card
   is — and on a full-width card on a 1920px monitor that is 720/180 -> ~450px of chart for a
   row of small integers. It looked fine at the width it was designed at and grew without bound
   on anything wider, which is why it read as "correct on my laptop, enormous on the monitor".

   So the box owns the height and the drawing fills it. --chart-height is a knob a caller can
   override per chart; the media query drops it on phones, where 200px of a 640px viewport is a
   different proposition. `align-items: stretch` on the grid already hands that height to both
   the y-axis gutter and the plot, so the HTML tick labels (positioned by percentage) stay put. */
.chart-plot {
    --chart-height: 200px;
    display: grid;
    grid-template-columns: 2.5rem 1fr;
    /* 🚨 THE ROW MUST BE DEFINITE, AND `height` ON THE CONTAINER IS NOT ENOUGH.
       With the row left implicit it is auto-sized from its content, and the SVG's `height: 100%`
       then has no definite row to resolve against — a cyclic dependency the spec resolves as
       `auto`. The intrinsic 4:1 ratio takes back over, the SVG is sized from its WIDTH, the row
       grows to that height while the container stays 200px, and the bars paint straight out of
       the card and over the panels below it (overflow is visible by default).
       minmax(0, 1fr) of a fixed-height container is definite before items are laid out, so
       `height: 100%` resolves to the 200px it was meant to. The 0 floor stops the row being
       pushed back up by the item's min-content height. */
    grid-template-rows: minmax(0, 1fr);
    align-items: stretch;
    height: var(--chart-height);
}

@media (max-width: 575.98px) {
    .chart-plot {
        --chart-height: 150px;
    }
}

.chart-y-axis {
    position: relative;
}

.chart-x-axis {
    position: relative;
    height: 1.15rem;
    margin-left: 2.5rem;
}

/* Fills the box in both axes. The components pass preserveAspectRatio="none", so the 720x180
   space is stretched rather than letterboxed — with `height: auto` and a max-height the drawing
   would instead shrink to fit and sit in the middle of two empty gutters.

   Stretching is safe for these two charts specifically: every mark is an axis-aligned rectangle,
   so a non-uniform scale makes bars wider or narrower and nothing else. The only casualty is the
   3-unit corner radius, which becomes very slightly elliptical. Do NOT copy this to a chart with
   circles, diagonals or text inside the SVG — those would visibly skew. */
.chart-svg {
    display: block;
    width: 100%;
    height: 100%;
    min-height: 0;
    /* Was `visible`, which is what turned a sizing mistake into bars painted across the two cards
       below. Every mark lives inside the viewBox, so there is nothing legitimate to show outside
       it, and clipping to its own viewport means any future mis-sizing stays a wrong-looking
       chart instead of leaking over the rest of the page. */
    overflow: hidden;
}

.chart-bar {
    fill: var(--chart-series, var(--chart-series-1));
}

.chart-grid-line {
    stroke: var(--chart-grid);
    stroke-width: 1;
    shape-rendering: crispEdges;
}

/* `fill` is kept alongside `color` so the class stays usable from inside an SVG if anything
   ever needs it again; the HTML labels read `color`. font-size is a real rem, never user units. */
.chart-axis-label {
    fill: var(--chart-axis-text);
    color: var(--chart-axis-text);
    font-size: 0.7rem;
    font-family: inherit;
    white-space: nowrap;
}

.chart-y-label {
    position: absolute;
    right: 0.4rem;
    transform: translateY(-50%);
}

/* The x ticks are anchored per label rather than always centred: centring the first and last
   ticks pushes half a label outside the plot at both ends. */
.chart-x-label {
    position: absolute;
    top: 0.1rem;
}

.chart-x-label.is-middle {
    transform: translateX(-50%);
}

.chart-x-label.is-end {
    transform: translateX(-100%);
}

/* Contextual band behind the marks — weekends, opening hours. Never a data encoding. */
.chart-band {
    fill: var(--chart-band);
}

/* Full-height transparent hit target per column: gives zero-value days/hours a tooltip and a
   hover affordance they would not have from a zero-height bar. */
.chart-hit {
    fill: transparent;
    cursor: default;
}
.chart-hit:hover {
    /* Its own token, NOT --chart-band. When these were the same colour, hovering a weekday
       painted it exactly like the weekend bands beside it, so pointing at the chart destroyed
       the one piece of context it carries. */
    fill: var(--chart-hover);
}

/* ── Drill-down: a column that filters the panels below it ────────────────────────────────────
   Only present when the chart is given an OnDaySelected handler, so a chart that does not drill
   in keeps a default cursor and no focus ring — an affordance that promises nothing is worse
   than none at all. */
.chart-hit-interactive {
    cursor: pointer;
}

/* The selected column stays marked whether or not the pointer is over it — hover alone would
   lose the marking the moment the user moved to read the panels the selection just filtered. */
.chart-hit-interactive.is-selected {
    fill: var(--chart-hover);
}

/* An SVG rect takes a focus ring from `outline` in current browsers, but Safari has historically
   not painted one — so the stroke is the ring that actually shows, and the outline is the one
   that matches the platform where it works. Both, deliberately. */
.chart-hit-interactive:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
    stroke: var(--accent);
    stroke-width: 2;
}

/* Days that are NOT the drilled-into one. Dimmed rather than hidden: the shape of the whole
   window is the context that makes one day worth looking at, and removing it would answer the
   question while destroying the reason for asking. */
.chart-bar.is-dimmed {
    opacity: 0.28;
}

.chart-empty {
    padding: 1.25rem 0;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* ── Outcome breakdown (horizontal bars) ── */
.outcome-chart {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* Label track is generous and the label WRAPS. This chart is deliberately single-hue with no
   legend, so the row label is the ONLY channel carrying series identity — clipping it to an
   ellipsis with a title tooltip (which does not exist on touch) meant "Never connected
   (busy/no answer)" and "Abandoned during the greeting" were both truncated, and with empty
   tracks at zero counts they were indistinguishable from each other and from "Not recorded".
   Wrapping costs a row of height; clipping costs the information. align-items:start keeps a
   two-line label top-aligned with its track. */
.outcome-row {
    display: grid;
    grid-template-columns: minmax(9rem, 38%) 1fr auto;
    align-items: center;
    gap: 10px;
}

.outcome-label {
    color: var(--text-secondary);
    font-size: 0.8rem;
    line-height: 1.25;
    overflow-wrap: break-word;
}

/* Below roughly a phone width the three-column grid cannot give the label a readable track at
   any percentage, so the label moves above its bar instead of competing with it. */
@media (max-width: 575.98px) {
    .outcome-row {
        grid-template-columns: 1fr auto;
        align-items: center;
        column-gap: 10px;
        row-gap: 2px;
    }

    .outcome-label {
        grid-column: 1 / -1;
    }
}

.outcome-track {
    height: 10px;
    background: var(--chart-grid);
    border-radius: 5px;
    overflow: hidden;
}

.outcome-fill {
    height: 100%;
    background: var(--chart-series, var(--chart-series-1));
    border-radius: 5px;
    transition: width 240ms ease;
}

/* A zero bucket is still a row with a label and a 0 — the empty track says so on its own,
   so the fill collapses rather than drawing a misleading stub. */
.outcome-fill.is-zero {
    background: transparent;
}

.outcome-value {
    color: var(--text-primary);
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
    min-width: 2.5ch;
    text-align: right;
}

/* ── Stat tile ── */
.stat-tile {
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--border-radius);
    padding: 1rem 1.1rem;
    height: 100%;
}

.stat-tile-label {
    color: var(--text-muted);
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 0.35rem;
}

.stat-tile-value-row {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* Text tokens, never a series hue: a coloured hero number implies an encoding it has not got. */
.stat-tile-value {
    color: var(--text-primary);
    font-size: 1.75rem;
    font-weight: 600;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.stat-tile-context {
    color: var(--text-muted);
    font-size: 0.75rem;
    margin-top: 0.35rem;
    line-height: 1.35;
}

/* ── Channel selector — the three volume tiles are a REAL tablist ──
   Home.razor's "Calls answered / Web chats / WhatsApp conversations" tiles choose what every panel
   below them is about. They are <button role="tab"> inside a role="tablist", not click handlers on
   divs, so they are reachable by Tab, activated by Enter/Space for free, and moved between with the
   arrow keys.

   THEY MUST LOOK INTERACTIVE BEFORE THEY ARE TOUCHED. A hover-only affordance is invisible to the
   person deciding whether to try clicking, and invisible to touch entirely. So a resting tab carries
   three things the three plain .stat-tile cards beside them do not: a pointer cursor, a raised
   shadow, and a full-width 3px ledge along its bottom edge that reads as a tab strip.

   THE RESTING LEDGE IS DRAWN IN --text-muted, NOT --border-strong, and that is the whole point of it.
   --border-strong is rgba(white,0.15) / rgba(black,0.15), which composites to #404040 on the dark
   card and #D9D9D9 on the light one — 1.61:1 and 1.41:1 against their own surface. That is below the
   3:1 WCAG 1.4.11 asks of a non-text UI element, i.e. not reliably visible, which for the ONE mark
   carrying "you can click this" is the whole affordance failing. The drop shadow cannot cover for it
   either: rgba(0,0,0,0.18) on the #121212 page is invisible in dark mode, where the app starts.
   --text-muted clears 3:1 on all four brand/theme combinations — measured, not assumed:
   motlogs dark #707070 3.37:1, team.help dark #949494 5.50:1, motlogs light #8B8B8B 3.41:1,
   team.help light #6B6B6B 5.33:1. Hover then steps up to --text-secondary (6.38:1 dark, 7.81:1
   light) so the three ledge states — resting, hover, selected — stay distinguishable from each
   other. No new hex: all three are existing frozen tokens.

   SELECTION IS NEVER CARRIED BY COLOUR ALONE. The selected tab changes its border colour AND its
   bottom ledge to the accent, AND shows the word "Showing" beside its label, AND is the only one with
   aria-selected="true". Anyone who cannot see the accent still has the word and the screen reader
   still has the state.

   THE ACCENT IS LEGITIMATE HERE — this is an interactive element, which is precisely what
   --accent is reserved for (see the chart-token block above: it is never a series colour). It is used
   only as a BORDER, a LEDGE and a FOCUS RING, never as a fill behind text: white-on-accent measures
   2.46–3.20:1 on the five brand accents in dark mode and fails AA outright. As a non-text UI element
   the accent clears the 3:1 requirement against the card surface on every brand and both themes —
   measured, not assumed: motlogs 5.21 / 5.16, harbor 5.21 / 6.70, evergreen 6.70 / 5.47,
   indigo 5.59 / 6.29, damson 6.77 / 8.24 (dark on #1E1E1E / light on #FFFFFF).

   The :focus-visible ring is measured SEPARATELY and against a DIFFERENT backdrop, because
   outline-offset: 2px lifts it off the tab and onto the page behind it (--bg-primary #121212 dark /
   #F9FAFB light), not the card surface: motlogs 5.86 / 4.94, harbor 5.85 / 6.41, evergreen
   7.53 / 5.24, indigo 6.28 / 6.02, damson 7.61 / 7.88. All ten clear 3:1. Re-measure this row, not
   just the row above, if the offset or the page background ever changes. */
.channel-tabs {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.75rem;
}

/* Two across on a tablet, four across on a desktop. Below 576px they stack full-width: four tiles
   sharing a 390px row leaves ~90px each, which truncates "WhatsApp conversations" and its context
   line to nothing.

   🪤 THIS SAID repeat(3, …) AND THE MEETINGS TILE MADE IT WRONG. A fourth tile in a three-column
   grid does not overflow or error — it wraps onto a second row ALONE, at one third width, with two
   thirds of the row empty beside it, on every desktop. Nothing fails; it just looks broken. The
   count here has to move with ChannelTabs, so if a fifth channel ever lands, change it here too.

   The 900px step exists because four tiles on a ~700px tablet is the same ~110px squeeze the
   sub-576px rule already avoids — two rows of two reads better than four cramped columns. */
@media (min-width: 576px) {
    .channel-tabs {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (min-width: 900px) {
    .channel-tabs {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

.channel-tab {
    appearance: none;
    -webkit-appearance: none;
    display: block;
    width: 100%;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--border-radius);
    padding: 1rem 1.1rem;
    height: 100%;
    /* The resting ledge + lift. The LEDGE is the load-bearing half — see the note above for why it is
       --text-muted and not --border-strong. The shadow is a bonus that only really reads in light
       mode. */
    box-shadow: inset 0 -3px 0 var(--text-muted), 0 1px 3px rgba(0, 0, 0, 0.18);
    transition: box-shadow 160ms ease, border-color 160ms ease, transform 160ms ease;
}

.channel-tab:hover {
    border-color: var(--border-strong);
    box-shadow: inset 0 -3px 0 var(--text-secondary), 0 4px 10px rgba(0, 0, 0, 0.22);
    transform: translateY(-1px);
}

/* Keyboard focus is visible and does not depend on the selected state. :focus-visible only, so a
   mouse click does not leave a ring behind. */
.channel-tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.channel-tab[aria-selected="true"] {
    border-color: var(--accent);
    box-shadow: inset 0 -3px 0 var(--accent), 0 1px 3px rgba(0, 0, 0, 0.18);
}

/* A selected tab does not lift on hover — it is already the current one, and the movement reads as
   "you can still pick this". */
.channel-tab[aria-selected="true"]:hover {
    transform: none;
}

/* A button may not contain block-level elements, so the tile's three lines are spans that have to be
   told to behave like the divs in .stat-tile. Typography is deliberately the SAME classes, so the
   selector row and the tiles under it are one visual family. */
.channel-tab .stat-tile-label,
.channel-tab .stat-tile-context {
    display: block;
}

.channel-tab-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

/* The non-colour half of the selected state. Text, in the normal foreground colour — not a coloured
   dot, which would put the whole signal back on hue. */
.channel-tab-flag {
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-secondary);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    padding: 0.05rem 0.4rem;
    white-space: nowrap;
}

/* Motion is decoration here: the ledge, the border and the word "Showing" carry every meaning. */
@media (prefers-reduced-motion: reduce) {
    .channel-tab {
        transition: none;
    }
    .channel-tab:hover {
        transform: none;
    }
}

/* ── Trend delta ──
   trend-up = good, trend-down = bad (flipped by LowerIsBetter in the component); the ARROW
   carries direction. Colour never travels alone — see the note in the token block above. */
.trend-delta {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    font-size: 0.78rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.trend-delta .trend-arrow {
    font-size: 0.65rem;
    line-height: 1;
}
.trend-up {
    color: var(--chart-trend-up);
}
.trend-down {
    color: var(--chart-trend-down);
}
.trend-flat {
    color: var(--text-muted);
}

/* ── Usage meter (spend vs cap) ── */
.usage-meter {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.usage-meter-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
}

.usage-meter-label {
    color: var(--text-muted);
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.usage-meter-value {
    color: var(--text-primary);
    font-size: 1.25rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.usage-meter-track {
    height: 8px;
    background: var(--chart-grid);
    border-radius: 4px;
    overflow: hidden;
}

.usage-meter-fill {
    height: 100%;
    border-radius: 4px;
    transition: width 240ms ease;
}

/* Status colours — deliberately the app's existing status tokens, NOT chart series hues. */
.usage-meter-fill.is-ok {
    background: var(--success);
}
.usage-meter-fill.is-warning {
    background: var(--warning);
}
.usage-meter-fill.is-over {
    background: var(--error);
}

.usage-meter-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    font-size: 0.75rem;
    color: var(--text-muted);
}
.usage-meter-foot.is-warning .usage-meter-state {
    color: var(--warning);
}
.usage-meter-foot.is-over .usage-meter-state {
    color: var(--error);
}

.usage-meter-caption {
    color: var(--text-muted);
}

/* ═══ Pager (<ListPager>) ═══════════════════════════════════════════════════════════════════════
   Bootstrap's .pagination ships white pills with blue links and no theme awareness, so on the dark
   default it sat as a bright white slab at the foot of every list — the one control on the page
   that had never been dressed.

   ⚠️ DARK IS THE BASE AND LIGHT IS THE OVERRIDE, matching the rest of this file: the palette lives
   on bare :root and html[data-theme="light"] redefines it. So these rules paint the DEFAULT (dark)
   look and the block below hands light mode back to Bootstrap untouched — which is what was asked
   for, and it means a change made for dark cannot regress light.

   Tokens rather than literals throughout, so the active page follows the brand accent on both
   brands (MOTLogs red, team.help purple) with no per-brand rule. */
.pagination .page-link {
    background-color: var(--bg-surface);
    border-color: var(--border);
    color: var(--text-primary);
    transition: var(--transition);
}

.pagination .page-link:hover {
    background-color: var(--bg-elevated);
    border-color: var(--border-strong);
    color: var(--text-primary);
}

/* Keyboard focus has to survive the recolour — Bootstrap's ring is tuned for a white ground and
   all but disappears on this one. */
.pagination .page-link:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    box-shadow: none;
}

.pagination .page-item.active .page-link {
    background-color: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* Disabled arrows dimmed rather than white-on-white. The ellipsis separator is the same
   .page-item.disabled .page-link shape, so one rule covers both. */
.pagination .page-item.disabled .page-link {
    background-color: var(--bg-surface);
    border-color: var(--border);
    color: var(--text-muted);
    opacity: 0.65;
}

/* The "Show N per page" select beside it, for the same reason — a stock <select> is white. */
.list-pager .form-select {
    background-color: var(--bg-surface);
    border-color: var(--border);
    color: var(--text-primary);
}

/* ── Light mode keeps Bootstrap's own look, deliberately unchanged ──
   `revert` returns each property to the rule Bootstrap set rather than to an initial value, so this
   is a genuine hand-back rather than a second theme to keep in step. */
html[data-theme="light"] .pagination .page-link,
html[data-theme="light"] .pagination .page-link:hover,
html[data-theme="light"] .pagination .page-item.active .page-link,
html[data-theme="light"] .pagination .page-item.disabled .page-link,
html[data-theme="light"] .list-pager .form-select {
    background-color: revert;
    border-color: revert;
    color: revert;
    opacity: revert;
}
