/* ──────────────────────────────────────────────────────────────
   page.css — composed on top of tokens.css.
   Covers Saved Tests + Reports + Custom Reports builder + every
   modal/popover/tooltip the prototype renders.

   ─────────── RESPONSIVE BREAKPOINT SYSTEM ───────────
   Five canonical viewport tiers — every @media rule in this file
   uses one of these exact widths. Adding a one-off width (e.g.
   980, 1080) is forbidden; snap the rule to the nearest tier
   instead. Component-internal restructure (e.g. filter-bar
   row-wrap, KPI strip) preferably uses intrinsic sizing
   (`auto-fit` grid, `flex-wrap`) so the layout responds to
   actual content rather than a viewport band.

     ≥ 1280            wide desktop  — full density (4-col grids)
     1101–1279         desktop       — T1 3-col, T2/T3 sidebar+main
                                       with independent scroll
     721–1100          tablet        — T1 2-col, single-page scroll
     481–720           mobile        — 1-col, compact padding,
                                       hamburger drawer, table h-scroll
     ≤ 480             small phone   — modal/table cell tightening

   Query conventions:
     `@media (max-width: 1279px)`   — below wide desktop
     `@media (max-width: 1100px)`   — below desktop (tablet & smaller)
     `@media (max-width: 720px)`    — mobile & smaller
     `@media (max-width: 480px)`    — small phone only
     `@media (min-width: 1101px)`   — desktop & wider (paired with 1100)

   Login page (login.html) uses its own page-specific breakpoints
   for form ergonomics and is intentionally exempted from this
   system.

   ─────────── TABLE OF CONTENTS ───────────
   (search by section comment to jump — line numbers shift)

   • Tooltip system          [data-tip] base + align variants
   • Layout shell            .app, .shell, .main, viewport scroll
   • Row actions             kebab menu, action button hover
   • Profile popover         peek card on student-name hover
   • Top nav                 .topnav-* + density toggle + bell
   • Client rail             left overlay drawer for institutes
   • Main column             page-canvas tone + padding
   • Filter bar              base + reports + templates variants
   • Time filter dropdown    presets list + calendar pane
   • Range band              calendar grid hover/select states
   • Data table              base, density-cozy, density-compact
   • Page header             title + sub + tabs slot + actions
   • Tabs                    underline-style, scrollable strip
   • Breadcrumbs
   • KPI strip + .kpi-sub    value / sub / icon-seg
   • Pass-rate cell + fail bar  + .pr-cell-tip floating card
   • Severity pill           Minor / Major
   • Status pill (unified)   active/paused/inactive/draft tones
   • Callout banner          info/warn/etc strip
   • Report doc-head         title row + level chain row
   • Level chain             drill-down pills + nav hint
   • Drill-down picker       popover from chain tail
   • Empty state             shared illustration + copy
   • Stepper                 7-step wizard nav (Custom Reports)
   • Reports page            tab body, doc, split panes,
                             builder, builder filters, custom
                             reports list, schedules list
   • Unified in-table tooltip  applied to action buttons + pills
   • Branding panel          (legacy class prefix `.tweaks-*`)
   • Responsive              breakpoints: 1279, 1079, 899, 599
   • Generate-report modal
   • Create-schedule modal
   • Save-template confirm modal
   • Schedule history modal
   ──────────────────────────────────────── */

/* The legacy CSS-pseudo `[data-tip]::after` tooltip is now SUPPRESSED.
   `native-title-shim.js` (loaded on every page) intercepts every
   `[data-tip]` hover/focus and renders the same portal-anchored
   ChartTip used by every other tooltip in the system. One aesthetic,
   no clip-prone pseudo-elements. The legacy `[data-tip]::after` rule
   below is kept for selector-coexistence with other style blocks but
   is content-cleared so it never renders.
   IMPORTANT: this kill-switch is SCOPED to `::after` only. An earlier
   version included `::before`, which silently nuked every legitimate
   `::before` pseudo on any element that happened to carry a
   `data-tip` (e.g. the vehicle-tag transmission dot — the dot's
   element gets `data-tip="Light Vehicle Automatic"` for the hover
   label, so `[data-tip]::before` matched + over-applied
   `content: none`). There is no legacy `[data-tip]::before` rule
   that needed killing — only `::after`. */
[data-tip]::after { content: none !important; }
/* Legacy rules below — kept for selector coexistence, content suppressed above. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: oklch(0.22 0 0);
  color: oklch(0.97 0 0);
  padding: 5px 9px;
  border-radius: 6px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  /* Tooltips must always render in sentence case with normal
     letter-spacing — never adopt the trigger element's
     `text-transform: uppercase` or letter tracking. Some
     triggers (e.g. .ive-map-panel-open, KPI labels, segmented
     toggle labels) intentionally use uppercase + tracking for
     their visible text; without an explicit reset here, those
     properties cascade into the ::after tooltip and produce
     hard-to-read all-caps tooltips. */
  text-transform: none;
  letter-spacing: normal;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 90ms ease;
  z-index: 200;
  box-shadow: 0 4px 10px -4px rgba(0,0,0,0.25);
  max-width: min(320px, 90vw);
  overflow: hidden;
  text-overflow: ellipsis;
}
[data-tip]:hover::after,
[data-tip]:focus-visible::after { opacity: 1; }
/* Topnav tooltips render below the bar so they aren't clipped by the
   viewport's top edge (e.g. the notifications bell). */
.topnav [data-tip]::after {
  bottom: auto;
  top: calc(100% + 6px);
}
/* Elements near the right edge anchor their tooltip to the right so
   it never overflows the viewport (e.g. density toggle, avatar). */
.topnav-tools > :last-child[data-tip]::after,
.filter-actions [data-tip]::after,
[data-tip-align="right"]::after {
  left: auto;
  right: auto;
  inset-inline-end: 0;
  transform: none;
}
/* Bottom-anchored tooltip variant — used by row-icon-btn instances
   inside table cells where the parent table-wrap has overflow-clip
   for border-radius rounding, which would otherwise hide the
   tooltip rendered above the button. Pointing the tooltip downward
   keeps it inside the row and visible. */
[data-tip-align="bottom"]::after {
  bottom: auto;
  top: calc(100% + 6px);
}
/* Inline-end flow — tooltip flows toward the map centre. Map
   controls anchor to the inline-end edge of the wrap (right in LTR,
   left in RTL), so the tooltip flowing to the inline-start side
   keeps it inside the visible viewport. Logical-only so the flip is
   automatic on `dir` change. */
[data-tip-align="left"]::after {
  bottom: auto;
  top: 50%;
  left: auto;
  right: auto;
  inset-inline-end: calc(100% + 8px);
  inset-inline-start: auto;
  transform: translateY(-50%);
}

/* ─────────── Layout shell ─────────── */
/* Force the vertical scrollbar to always be present so the page never
   shifts horizontally when content height changes (filtering a table down,
   switching to a shorter tab, etc.).
   - macOS Chrome/Safari/Firefox with overlay scrollbars: stays overlay,
     visible only while scrolling — no visible track, no shift.
   - Windows / Linux / macOS with "Always show scrollbars": permanent track,
     no shift, no awkward "ghost gap" that scrollbar-gutter would leave.
   This avoids the Safari quirk where `scrollbar-gutter: stable` reserves
   visible empty space without filling it with a scrollbar. */
html { overflow-y: scroll; }
/* Login page is the exception — its content is fixed-height (sign-in
   card on a centered canvas, no dynamic-height tables or tabs) so the
   "always-on scrollbar" rule above just leaves an unused gutter on
   the trailing edge. Override to `auto` so the scrollbar only appears
   if the viewport is shorter than the card, and disappears otherwise. */
html:has(body.auth-body) { overflow-y: auto; }

/* ─────────────────────────────────────────────────────────────
   GLOBAL CURSOR DEFAULTS
   Browsers default to the I-beam (text) cursor over everything
   that contains text — but in a controls-heavy operator dashboard,
   most text is informational (names, values, badges, headers),
   not "select me" content. The I-beam suggests selectability
   where it isn't meaningful and creates inconsistency between
   interactive and non-interactive surfaces. Reset to `default`
   (arrow) globally, then let real input surfaces (typing inputs,
   contenteditable) get the text cursor, and clickable elements
   (buttons, links, etc) explicitly opt back in to `pointer`.

   Selection still works — users can still drag-select or
   triple-click any text to copy values. They just don't get the
   I-beam hover hint, which trades a small affordance loss for
   system-wide visual consistency.
   ───────────────────────────────────────────────────────────── */
body { cursor: default; }

/* Typing surfaces keep the text cursor — the I-beam is the right
   affordance for "you can type/edit here." */
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="file"]):not([type="color"]):not([type="range"]),
textarea,
[contenteditable="true"] { cursor: text; }

/* Interactive elements get the pointer cursor. Exclusions account
   for disabled state (handled by the disabled rule below). */
a[href],
button:not([disabled]),
[role="button"]:not([aria-disabled="true"]),
label[for],
select,
summary { cursor: pointer; }

/* Disabled / inactive elements show the universal not-allowed
   cursor — clearer than just leaving the default. */
[disabled],
[aria-disabled="true"] { cursor: not-allowed; }

/* ─────────────────────────────────────────────────────────────
   GLOBAL :focus-visible SAFETY NET (a11y baseline)
   Every keyboard-focused interactive element MUST show a visible
   focus ring. Many components in this codebase have `outline: 0`
   to suppress the OS default — this rule re-applies a brand-colored
   ring on `:focus-visible` only (so mouse clicks still don't show
   it, only keyboard tabbing does).
   Specific components can still override with their own
   `:focus-visible` rule (e.g. for tighter offsets or alternate
   colors); this is the floor.

   Per `project_design_system_master.md` accessibility commitment:
   "every interactive element has a :focus-visible state. No
   `outline: 0` without a tested fallback."
   ───────────────────────────────────────────────────────────── */
:where(button, a, input, select, textarea, [tabindex], [role="button"],
       [role="link"], [role="tab"], [role="menuitem"]):focus-visible {
  outline: 2px solid var(--brand-600, oklch(0.52 0.16 240));
  outline-offset: 2px;
  border-radius: 3px;
}

.app {
  min-height: 100vh;
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--bg);
  overflow-x: clip;
}
/* Dashboard ambient brand bloom — dark mode only.
   Same composition as the login-page corner bloom but at roughly half
   the intensity (18% / 12% vs. the login's 38% / 28%) and flipped to
   the *opposite* diagonal: top-leading + bottom-trailing instead of
   top-trailing + bottom-leading. The reason is anchoring — the bloom
   reads as if it's emanating downward from the tenant logo (top of
   the leading edge of the topnav) and bracketing the page H1 below
   it. The bottom-trailing bloom balances the diagonal so the canvas
   doesn't feel one-sidedly lit.

   The login is allowed to be cinematic because it's the doorway with
   one card on a mostly empty canvas. The dashboard is the workplace,
   with dense data surfaces — too much atmospheric color competes with
   chart axes and status pills. At these percentages the bloom only
   registers in page margins and between cards, never bleeding into
   table contrast.

   `background-attachment: fixed` anchors the gradient to the viewport
   so the blooms stay at the viewport corners as the user scrolls,
   rather than sliding off the top with `.app`'s content height.

   RTL flip: in Arabic the tenant logo and page H1 mirror to the right
   side of the topnav, so the blooms must mirror with them. CSS
   percentage positions in radial-gradient aren't writing-direction
   aware (they're absolute on the canvas), so we explicitly swap the
   stops under `[dir="rtl"]`.

   Light mode is intentionally unscoped — the existing light bg-sunken
   plus brand-tinted topnav/KPIs already carries enough brand presence. */
[data-theme="dark"] .app {
  background-image:
    /* top-leading bloom — sits under the tenant logo and page H1 */
    radial-gradient(1200px 600px at -10% -10%, color-mix(in oklab, var(--brand-500) 18%, transparent), transparent 60%),
    /* bottom-trailing bloom — diagonal balance */
    radial-gradient(900px 600px at 110% 110%, color-mix(in oklab, var(--brand-500) 12%, transparent), transparent 60%);
  background-attachment: fixed;
}
[data-theme="dark"][dir="rtl"] .app {
  background-image:
    /* top-leading in RTL = top-right (where the logo + H1 now live) */
    radial-gradient(1200px 600px at 110% -10%, color-mix(in oklab, var(--brand-500) 18%, transparent), transparent 60%),
    /* bottom-trailing in RTL = bottom-left */
    radial-gradient(900px 600px at -10% 110%, color-mix(in oklab, var(--brand-500) 12%, transparent), transparent 60%);
}

.shell {
  display: grid;
  grid-template-columns: 1fr;        /* main content always takes the full width */
  gap: 0;
  min-height: 100vh;
  position: relative;
  overflow-x: clip;
}

/* ── Institute switcher rail offset ───────────────────────────────
   The driving-school .client-rail is positioned `inset-inline-start:
   var(--rail-offset)` so it anchors flush against the inside edge
   of the SideNav (not the viewport edge — that would collide with
   the sidenav). The value tracks the current sidenav width:
     • desktop default → --sidenav-w-full  (256px)
     • sidenav collapsed manually → --sidenav-w-rail (64px)
     • tablet auto-rail (721–1100) → --sidenav-w-rail
     • mobile (≤768) → 0  (sidenav is a drawer that's closed
       by default; rail uses the full left edge instead)
   The same variable also positions .rail-backdrop so the sidenav
   stays visible AND interactive while the rail is open. */
:root { --rail-offset: var(--sidenav-w-full); }
:root[data-sidenav-collapsed] { --rail-offset: var(--sidenav-w-rail); }
@media (min-width: 721px) and (max-width: 1100px) {
  :root { --rail-offset: var(--sidenav-w-rail); }
}
@media (max-width: 720px) {
  /* At mobile the sidenav is an overlay drawer (not a layout column),
     so chrome anchored via --rail-offset must collapse to 0 regardless
     of the user's sidenav state. Match the (0,1,1) specificity of the
     `:root[data-sidenav-collapsed|expanded]` rules above so this wins
     the cascade at mobile width — without these explicit selectors the
     0,0,1 `:root` alone gets overridden by the persisted attribute and
     fixed-position chrome (compact-strip, client-rail, etc.) starts
     64px from the viewport's leading edge. */
  :root[data-sidenav-collapsed],
  :root[data-sidenav-expanded],
  :root { --rail-offset: 0px; }
}

/* Driving-school rail overlay positioning — see the consolidated block at
   "Client Rail" further down for the visuals. These rules just make the
   rail slide off-screen when closed.
   The transform must move the rail by its own width PLUS the
   inline-start offset so the rail clears the sidenav entirely when
   hidden (otherwise its rectangle would overlap the sidenav at z=47
   and visually cover it). */
.shell.rail-collapsed .client-rail {
  transform: translateX(calc(-100% - var(--rail-offset, var(--sidenav-w-full))));
  box-shadow: none;
  pointer-events: none;
}
[dir="rtl"] .shell.rail-collapsed .client-rail {
  transform: translateX(calc(100% + var(--rail-offset, var(--sidenav-w-full))));
}

/* Dimmed backdrop while the rail is open. Starts at `--rail-offset`
   so the sidenav stays visible AND interactive — user can still
   navigate away without first closing the rail. The backdrop catches
   clicks on the main content area (= close rail), and Esc / the
   rail-close button work as before. */
.rail-backdrop {
  position: fixed;
  inset-block-start: 0;
  inset-inline-start: var(--rail-offset, var(--sidenav-w-full));
  inset-inline-end: 0;
  inset-block-end: 0;
  background: rgba(15, 23, 42, 0.35);
  z-index: 46;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-fast);
}
[data-theme="dark"] .rail-backdrop { background: rgba(0, 0, 0, 0.55); }
.shell:not(.rail-collapsed) .rail-backdrop {
  opacity: 1;
  pointer-events: auto;
}

/* School switcher — clickable page-title that opens the rail.
   Sizes to content so the institute name sits flush next to the logo
   instead of floating in a wide pill. */
.school-switcher {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 4px 14px 4px 5px;
  max-width: min(560px, 100%);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  cursor: pointer;
  font-family: inherit;
  color: var(--ink);
  align-self: flex-start;
  margin-bottom: 4px;
  transition: background var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast);
  -webkit-tap-highlight-color: transparent;
}
[dir="rtl"] .school-switcher { padding: 4px 5px 4px 14px; }
.school-switcher:hover {
  background: var(--bg-raised);
  border-color: var(--line);
  box-shadow: var(--sh-xs);
}
/* Static identity chip — single-institute tenants, where there's nothing to
   switch to. Same logo + name (the chevron/hint is omitted in markup), but
   kill the interactive affordances: default cursor, no hover lift. */
.school-switcher--static { cursor: default; }
.school-switcher--static:hover { background: transparent; border-color: transparent; box-shadow: none; }
/* Open state — only the chevron rotates and the hint brightens.
   The brand-tinted background was reading as a "stuck pressed"
   button while the rail was open, so it's intentionally omitted. */
.school-switcher .page-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
[dir="rtl"] .school-switcher .page-title { font-size: var(--fsz-h3); }
.school-switcher-mark {
  width: 36px; height: 36px;
  flex-shrink: 0;
  border-radius: 10px;
  display: grid; place-items: center;
  font-size: var(--fsz-label); font-weight: 700; letter-spacing: .02em;
  color: white;
  position: relative; overflow: hidden;
}
.school-switcher-mark::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(255,255,255,0.18), rgba(0,0,0,0.08));
}
.school-switcher-mark > span { position: relative; z-index: 1; }
.school-switcher-mark[data-tone="A"] { background: oklch(0.55 0.16 240); }
.school-switcher-mark[data-tone="B"] { background: oklch(0.60 0.17 18); }
.school-switcher-mark[data-tone="C"] { background: oklch(0.55 0.13 160); }
.school-switcher-mark[data-tone="D"] { background: oklch(0.50 0.16 285); }
.school-switcher-mark[data-tone="E"] { background: oklch(0.55 0.14 205); }
.school-switcher-mark[data-tone="F"] { background: oklch(0.65 0.14 45); }
.school-switcher-mark[data-tone="G"] { background: oklch(0.58 0.14 325); }
.school-switcher-mark[data-tone="H"] { background: oklch(0.55 0.12 185); }
.school-switcher-mark[data-tone="I"] { background: oklch(0.55 0.12 120); }
.school-switcher-mark[data-tone="J"] { background: oklch(0.55 0.16 255); }
.school-switcher-mark[data-tone="K"] { background: oklch(0.45 0.07 35); }

/* Logo variant for the school switcher — naked image (no plate, no glow),
   anchored to the inline end so it sits flush beside the name. A plate read
   as a harsh white box on the always-dark wall, so the logo is left plain
   (a dark logo can go low-contrast on dark — accepted). Same lockup
   everywhere the shared switcher renders. */
.school-switcher-mark.school-switcher-mark--logo {
  width: 50px; height: 32px;
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
  display: block;
}
.school-switcher-mark.school-switcher-mark--logo::after { display: none; }
.school-switcher-mark.school-switcher-mark--logo > img {
  width: 100%; height: 100%;
  object-fit: contain;
  object-position: right center;
  padding: 0;
  display: block;
}
[dir="rtl"] .school-switcher-mark.school-switcher-mark--logo > img { object-position: left center; }
/* Favorite indicator — small filled gold star next to the
   institute name when the current institute is the user's
   favorite. The ClientRail's star toggle picks one favorite at a
   time; this surfaces that state in the closed switcher so the
   user knows their current scope without re-opening the rail.
   Decorative only — toggling still happens in the rail. */
.school-switcher-fav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Warm gold — same family as standard "starred" indicators
     across the web (Slack, Gmail, GitHub). High enough chroma to
     read as "marked" at a glance, low enough lightness to stay
     readable against the page bg. */
  color: oklch(0.78 0.15 80);
  flex-shrink: 0;
  margin-inline-start: 2px;
}
[data-theme="dark"] .school-switcher-fav {
  /* Lift in dark mode so the gold reads with the same vivacity as
     in light mode against the darker surrounding chrome. */
  color: oklch(0.85 0.16 80);
}

/* ── Stacked switcher variant (branch-scoped pages) ────────────
   When a page operates on a specific BRANCH (not just an institute) —
   e.g. Yard Live Tests — the switcher renders two-tier identity:
   the institute name as a small caption ABOVE the branch name (h2).
   Used by `<PageHeader>` when the optional `branch` prop is set. */
.school-switcher.is-stacked .school-switcher-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  line-height: 1.15;
}
.school-switcher.is-stacked .school-switcher-inst {
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-2);
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-inline-size: 100%;
}
.school-switcher.is-stacked .school-switcher-branch-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.school-switcher.is-stacked .page-title {
  /* Branch is now the primary identity — keep the h3 type scale
     used on the institute-only variant so the two layouts match
     visually in the header strip. */
  font-size: var(--fsz-h3);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  margin: 0;
}

/* Same favorite-star treatment in the compact (scrolled) strip — the
   star follows the institute name so the favorite signal stays
   visible after the page-header rolls out of view. 12px to sit
   proportional inside the 40px-tall strip. */
.compact-strip-fav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: oklch(0.78 0.15 80);
  flex-shrink: 0;
  margin-inline-start: 2px;
}
[data-theme="dark"] .compact-strip-fav { color: oklch(0.85 0.16 80); }

/* Subtle "switch institute" hint sitting after the institute name.
   Dimmed by default so the institute name stays the visual anchor;
   brightens to full strength when the user hovers the whole CTA. */
.school-switcher-hint {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  letter-spacing: 0;
  white-space: nowrap;
  flex-shrink: 0;
  opacity: 0.85;
  transition: color var(--t-fast), opacity var(--t-fast);
}
[dir="rtl"] .school-switcher-hint { font-size: var(--fsz-label); }
.school-switcher:hover .school-switcher-hint {
  color: var(--ink-2);
  opacity: 1;
}
.school-switcher[aria-expanded="true"] .school-switcher-hint {
  color: var(--brand-700);
  opacity: 1;
}
.school-switcher-chev {
  color: inherit;
  transition: transform var(--t-fast);
  flex-shrink: 0;
  opacity: 0.85;
}
.school-switcher[aria-expanded="true"] .school-switcher-chev {
  transform: rotate(180deg);
}

/* ── Row actions (per-row kebab menu) ──
   Keep the inline-end gap equal to the inline-start gap of the first
   (Traffic File) cell — they're both 10px via the .data-table td:first/last-child
   rules, so we just align the kebab to the trailing edge of the cell. */
.cell-actions { text-align: end; padding-inline-start: 4px; }
.row-actions { position: relative; display: inline-flex; }
.row-actions-btn {
  /* sm icon-only tier — compact for tables. See tokens.css `--cta-h-*`. */
  width: var(--cta-h-sm); height: var(--cta-h-sm);
  display: inline-grid; place-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-3);
  cursor: pointer;
  padding: 0;
  /* Always present — faded by default on desktop, full opacity on
     row hover. Touch / ≤1100px overrides below pin it to full
     opacity always (no hover capability there). */
  opacity: 0.4;
  transition: opacity var(--t-fast), background var(--t-fast), color var(--t-fast);
}
.data-table tbody tr:hover .row-actions-btn:not(.is-disabled),
.row-actions-btn.is-open,
.row-actions-btn:focus-visible:not(.is-disabled) { opacity: 1; }
/* Disabled — used for Absent rows where there's no replay or
   certificate to act on. Same visual slot so the column stays
   rhythmic; lower opacity + not-allowed cursor communicate
   inactivity. Comes BEFORE the touch override below so the touch
   media query can lift it to a slightly higher 0.45 on small
   viewports where the row stays statically visible. */
.row-actions-btn.is-disabled,
.row-actions-btn[disabled] {
  cursor: not-allowed;
  opacity: 0.25;
}
/* Touch / small-viewport: full opacity always — hover-fade only
   makes sense on devices that can hover. `hover: none` covers
   phones / touch-only tablets; `max-width: 1100px` covers mobile
   / tablet emulation in desktop browsers. Disabled stays muted
   so Absent rows still read as inactive on touch. */
@media (hover: none), (max-width: 1100px) {
  .row-actions-btn { opacity: 1; }
  .row-actions-btn.is-disabled,
  .row-actions-btn[disabled] { opacity: 0.45; }
}
.row-actions-btn:hover:not(.is-disabled) { background: var(--brand-tint); color: var(--brand-700); }
.row-actions-btn.is-open {
  background: var(--brand-tint-2);
  color: var(--brand-700);
}

.row-actions-menu {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  min-width: 220px;
  padding: 4px;
  display: flex; flex-direction: column;
  gap: 1px;
  animation: dropdown-in 120ms ease-out;
}
.row-actions-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-size: var(--fsz-label);
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  white-space: nowrap;
}
[dir="rtl"] .row-actions-item { font-size: var(--fsz-body); }
.row-actions-item:hover { background: var(--brand-tint); color: var(--brand-700); }
.row-actions-item svg { color: var(--brand-600); flex-shrink: 0; }
/* Link-state resets — `.row-actions-item` is rendered as <a> (to
   support cmd-click / middle-click into a new tab, see comment
   on the JSX in components.jsx RowActionsMenu). Without these
   the browser default underlines the label and drifts visited
   links to purple. Other menu-item families (.profile-menu-item,
   .filter-menu-item, .cols-menu-item) use <button> so don't need
   this. */
.row-actions-item,
.row-actions-item:link,
.row-actions-item:visited {
  text-decoration: none;
  color: var(--ink);
}
.row-actions-item:hover,
.row-actions-item:hover:visited {
  color: var(--brand-700);
}

/* ── Profile popover (peek card) ── */
.profile-popover {
  width: 360px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  /* Cap height to whatever fits between the trigger and the nearest
     viewport edge, minus a small margin. JS anchors by the closest
     edge (top when below, bottom when above), so this max keeps the
     popover within the viewport even on short screens — the body
     becomes scrollable rather than overflowing off-page. */
  max-height: calc(100vh - 24px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: dropdown-in 120ms ease-out;
}
.profile-popover .profile-info {
  overflow-y: auto;
  /* Keep scrollbar inside the rounded card so it doesn't sit on top
     of the bottom-radius. */
  scrollbar-gutter: stable;
}
.profile-header {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 14px 12px;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--brand-500) 4%, var(--bg-raised));
}
[data-theme="dark"] .profile-header { background: color-mix(in srgb, var(--brand-500) 8%, var(--bg-raised)); }
.profile-photo {
  width: 56px; height: 56px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--bg-sunken);
  /* 1px ring comes from --avatar-ring (system rule above). */
}
.profile-names { min-width: 0; flex: 1; }
.profile-name {
  font-size: var(--fsz-h3); font-weight: 600; color: var(--ink);
  line-height: 1.25;
  white-space: nowrap; overflow: hidden;
  /* No text-overflow:ellipsis — the marquee uses a fade-gradient mask
     and slides on hover instead. */
}
[dir="rtl"] .profile-name { font-size: var(--fsz-h3); }
/* Secondary-language name — always aligned to the page's start side
   (left in EN, right in AR). Inherits page direction explicitly so the
   bidi algorithm doesn't infer it from the Arabic content. */
.profile-name-sub {
  font-size: var(--fsz-caption); color: var(--ink-3);
  line-height: 1.3; margin-top: 1px;
  white-space: nowrap; overflow: hidden;
  /* No text-overflow:ellipsis — same marquee treatment as the primary. */
  font-family: var(--font-ar);
  direction: inherit;
  text-align: start;
}
.profile-meta {
  font-size: var(--fsz-caption); color: var(--ink-3);
  margin-top: 4px;
  line-height: 1.4;
  /* Allow up to 2 lines so longer "Institute - Branch" text reads cleanly. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.profile-meta-label { color: var(--ink-4); }
[dir="rtl"] .profile-meta { font-size: var(--fsz-label); }

/* Key-value info list */
.profile-info {
  margin: 0;
  padding: 6px 14px 4px;
  display: flex; flex-direction: column;
}
.profile-info-row {
  display: grid;
  grid-template-columns: 96px 1fr;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}
.profile-info-row:last-child { border-bottom: 0; }
/* Key + value share the same `--fsz-label` tier — visual hierarchy
   comes from color (ink-3 muted vs ink) and weight, not from case.
   The earlier uppercase + tracked-out caption treatment shouted at
   the user and made label-heavy popovers feel busy; normal-case
   labels read as field names without competing with the value. */
.profile-info-key {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
  margin: 0;
  letter-spacing: 0;
  text-transform: none;
}
[dir="rtl"] .profile-info-key { font-size: var(--fsz-label); }
.profile-info-val {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  margin: 0;
  min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .profile-info-val { font-size: var(--fsz-label); }
.profile-attempt-num {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--ink);
}
.profile-attempt-of { color: var(--ink-3); font-weight: 500; }

/* Inline value with copy button */
.profile-info-copy {
  display: flex; align-items: center; gap: 6px;
}
.profile-info-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Inline "leading icon + label" pattern — used in the L4 student/
   examiner grid AND the saved-tests profile popover to put a small
   visual cue (gender silhouette, country flag) in front of the value
   text. The value cell becomes inline-flex so the icon and label sit
   on the same baseline. The leading-icon wrapper inherits the muted
   ink so SVG glyphs match the surrounding type colour; the leading-
   flag wrapper bumps font-size slightly so the emoji's visual cap
   matches the text's. */
.info-with-leading {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Override the parent cell's nowrap/ellipsis so the icon doesn't
     get clipped on narrow popovers — the label can still ellipsis
     via the inner span if needed. */
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.info-leading-icon {
  display: inline-flex;
  align-items: center;
  color: var(--ink-3);
  flex-shrink: 0;
}
.info-leading-icon svg { display: block; }
.info-leading-flag {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  font-size: var(--fsz-h3);
  line-height: 1;
}
[dir="rtl"] .info-leading-flag { font-size: var(--fsz-h3); }
/* Phone numbers, emails, IDs — Latin/numeric content stays LTR even in
   Arabic mode so the leading "+" and spaces don't get re-ordered by bidi. */
.profile-info-val [dir="ltr"] { unicode-bidi: isolate; }

/* ── Profile popover · maneuvers block ─────────────────────────────
   Renders ONLY when a caller passes the `maneuvers` prop — currently
   only yardlive's AssignedCard + assigned-strip variant (the live-
   test left-side grid). Gives supervisors a sequence preview before
   they have to assign the student to the stage. Visually a stack of
   numbered rows (not pills) to make the order self-evident; pills
   suggest unordered tags, which is wrong for a maneuver sequence. */
.profile-maneuvers {
  border-top: 1px solid var(--line);
  padding: 10px 14px 12px;
}
.profile-maneuvers-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.profile-maneuvers-label {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
}
.profile-maneuvers-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  font-size: var(--fsz-tiny, 11px);
  font-weight: 600;
  color: var(--ink-3);
  background: color-mix(in oklab, var(--ink-3) 10%, transparent);
  border-radius: 999px;
  line-height: 1;
}
.profile-maneuvers-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
/* Pill chip — visual recipe mirrors `.ylc-assign-maneuver-pill` on
   the assigned card so the popover reads as the same component
   family. Fixed padding (no cqi clamps) because the popover width
   is fixed at 360px — no container-query scaling needed. Order is
   implicit via flow direction; no ordinal number to avoid being
   confused with the "Attempts: N of M" row above. */
.profile-maneuvers-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  background: color-mix(in oklab, var(--brand-500) 8%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  border-radius: var(--r-pill, 999px);
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  line-height: 1.5;
  /* Long Arabic names ("الموقف العمودي") read better as wrapped
     pills than as cropped ones. nowrap on each chip; the row
     flex-wraps so multi-chip rows stack cleanly. */
  white-space: nowrap;
}
[data-theme="dark"] .profile-maneuvers-pill {
  background: color-mix(in oklab, var(--brand-500) 12%, var(--bg-raised));
  border-color: color-mix(in oklab, var(--brand-500) 28%, var(--line));
}
.profile-copy {
  width: 22px; height: 22px;
  display: inline-grid; place-items: center;
  flex-shrink: 0;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--ink-3);
  cursor: pointer;
  padding: 0;
  transition: all var(--t-fast);
}
.profile-copy:hover {
  background: var(--brand-tint);
  color: var(--brand-700);
  border-color: color-mix(in oklch, var(--brand-500) 30%, var(--line));
}
.profile-copy.is-copied {
  background: color-mix(in oklab, var(--ok, oklch(0.55 0.13 155)) 14%, transparent);
  border-color: color-mix(in oklab, var(--ok, oklch(0.55 0.13 155)) 35%, var(--line));
  color: var(--ok, oklch(0.55 0.13 155));
}

/* Popover footer link — shared shape for the "View profile" CTA
   on `.profile-popover` (student / examiner cards) AND the
   "View Request Details" CTA on `.ovr-card` (override progress
   card). Reads as a calm text link, not a brand-filled button —
   the popover itself is the primary surface; the CTA is just
   "open the full page if you want more". Full-bleed inside the
   card so the click target stretches across the card width;
   border-top separates it from the info grid above. */
.profile-foot-link,
.ovr-card-foot-link {
  display: flex;
  align-items: center;
  /* Leading-edge anchored — text + arrow read as one inline link at
     the start of the footer, matching the leading-edge anchoring of
     the popover itself (and how Linear / Stripe / Notion lay out
     "navigate to full view" CTAs in their info popovers). Centered
     felt heavy when the link was the only thing in the footer. */
  justify-content: flex-start;
  gap: 6px;
  inline-size: 100%;
  padding: 12px 14px;
  border: 0;
  border-block-start: 1px solid var(--line);
  background: transparent;
  font-size: var(--fsz-label);
  font-weight: 600;
  font-family: inherit;
  color: var(--brand-700);
  text-decoration: none;
  cursor: pointer;
  text-align: start;
}
.profile-foot-link:hover,
.profile-foot-link:focus-visible,
.ovr-card-foot-link:hover,
.ovr-card-foot-link:focus-visible {
  text-decoration: underline;
  background: color-mix(in srgb, var(--brand-500) 4%, transparent);
}
.profile-foot-link:focus-visible,
.ovr-card-foot-link:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: -2px;
}

[dir="rtl"] .shell { direction: rtl; }

/* ─────────── Top nav ─────────── */
.topnav {
  height: 64px;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--line);
  display: flex;
  justify-content: center;
  position: sticky;
  top: 0;
  z-index: 50;
}
/* Inner wrapper caps the nav content at 1600 px (matches the page-content
   cap on `.main > *`) while the .topnav background continues edge-to-edge
   so wide viewports still feel anchored. Below 1600 px the cap has no
   effect — the content just fills the available width. */
.topnav-inner {
  width: 100%;
  max-width: 1600px;
  height: 100%;
  display: grid;
  grid-template-columns: 128px 1fr auto;
  align-items: center;
  padding-inline: 16px;
  gap: 16px;
}

.hamburger-btn {
  /* md icon-only tier — matches `.icon-btn` siblings in topnav.
     See tokens.css `--cta-h-*`. */
  width: var(--cta-h-md); height: var(--cta-h-md);
  display: none;
  place-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink);
  cursor: pointer;
  margin-inline-end: 4px;
}
.hamburger-btn:hover { background: var(--brand-tint); }

.topnav-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  padding: 0;
  /* Pin the brand cell to the logo's exact footprint so it can't
     pick up stray width from descendants. */
  inline-size: 128px;
  block-size: 32px;
}
.tenant-icon {
  width: 128px;
  height: 32px;
  padding: 0;
  /* Tenant logo — driven by boot.js via --logo-tenant-* (set per active
     tenant). Falls back to the Performance Labs logo if boot.js hasn't
     run. Light/dark swap stays in CSS (the dark rule below). */
  background-image: var(--logo-tenant-light, url('assets/tenants/pl/PL-logo-light.svg'));
  background-repeat: no-repeat;
  background-size: contain;
  background-position: left center;
}
/* In Arabic the logo's frame sits on the right of the bar, so anchor the
   image to the inside (right) edge of its frame instead of the outer (left)
   edge — otherwise the logo image visually drifts away from the hamburger. */
[dir="rtl"] .tenant-icon { background-position: right center; }
[data-theme="dark"] .tenant-icon {
  background-image: var(--logo-tenant-dark, url('assets/tenants/pl/PL-logo-dark.svg'));
}
.brand-mark {
  width: 30px; height: 30px;
  border-radius: 9px;
  background: var(--brand-600);
  color: var(--brand-ink);
  display: grid; place-items: center;
  box-shadow: var(--sh-brand);
}
.brand-text {
  display: flex; align-items: center; gap: 8px;
  min-width: 0;
}
.brand-name {
  font-weight: 700;
  font-size: var(--fsz-h2);
  letter-spacing: -0.02em;
  color: var(--ink);
}
.brand-dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--ink-4);
}
.brand-client {
  display: inline-flex; align-items: center; gap: 4px;
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--fsz-body);
  font-weight: 500;
  transition: background var(--t-fast);
  min-width: 0;
}
.brand-client:hover { background: var(--brand-tint); color: var(--ink); }
.brand-client > span:first-child {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 140px;
}

.topnav-items {
  display: flex; align-items: center; gap: 2px;
  justify-content: center;
  flex-wrap: nowrap;
  overflow: hidden;
}
.nav-item {
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent;
  border: 0;
  padding: 8px 11px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--fsz-body);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
  white-space: nowrap;
  text-decoration: none;          /* anchors render as nav buttons */
  font-family: inherit;
}
[dir="rtl"] .nav-item { font-size: var(--fsz-body); }
.nav-item:hover { background: var(--brand-tint); color: var(--ink); }
.nav-item.is-active {
  background: var(--brand-tint-2);
  color: var(--brand-700);
}

/* Module icons (topnav, mobile menu) are now inline SVGs in the
   `Icon` component — see components.jsx. The previous mask-based
   `.nav-icon-asset` / `.nav-icon--<id>` system was retired so module
   icons read from the same source as the rest of the inline icons.
   The `.nav-item > svg` rule below picks them up via element type. */
[data-theme="dark"] .nav-item.is-active { color: var(--brand-700); }
.nav-item svg { opacity: 0.85; }

.topnav-tools {
  display: flex; align-items: center; gap: 6px;
}

.icon-btn {
  /* md icon-only tier — pulls from --cta-h-md so topnav icons,
     filter-bar icon buttons, kebab menus, etc. all share one
     height definition. See tokens.css `--cta-h-*`. */
  width: var(--cta-h-md); height: var(--cta-h-md);
  display: inline-grid; place-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  position: relative;
  transition: background var(--t-fast), color var(--t-fast);
}
.icon-btn:hover { background: var(--brand-tint); color: var(--ink); }
/* "Has notification" state — applied by TopNav when `bellUnread` is
   true. Shifts the bell from neutral ink to the brand color so the
   filled silhouette reads as active. The red dot indicator below
   carries the secondary "alert" signal. The hover rule above keeps
   precedence (last-defined for the same selector specificity), so
   hovering an active bell pulls it to full ink — same as resting
   state — which makes the brand → ink shift on hover read as a
   subtle "you're about to interact" cue. */
.icon-btn--has-notif { color: var(--brand-600); }

.dot-indicator {
  position: absolute; top: 6px; inset-inline-end: 7px;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--err);
  border: 2px solid var(--bg-raised);
}

.avatar {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--brand-200);
  color: var(--brand-900);
  display: inline-grid; place-items: center;
  font-size: var(--fsz-caption); font-weight: 700;
  letter-spacing: .03em;
  cursor: pointer;
  margin-inline-start: 4px;
  border: 0;
  font-family: inherit;
  transition: box-shadow var(--t-fast);
  box-shadow: var(--avatar-ring);
}
.avatar:hover { box-shadow: var(--avatar-ring), 0 0 0 3px var(--brand-tint); }
/* Photo fill for the base avatar (the bespoke avatar classes set this on
   their own `img`; the shared <Avatar> needs it on the base too). */
.avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; border-radius: inherit; display: block; }

/* ── System rule: every CIRCULAR USER-IDENTITY avatar gets the
   same 1px inset ring (see --avatar-ring in tokens.css). Prevents
   white photos on white cards from disappearing without an edge.
   Excluded by design:
     - .tenant-icon (rectangular BRAND logo, not an avatar)
     - .inst-card-tile--logo / .client-mark--logo (photo-frame
       tiles that already carry their own hairline border for the
       white-canvas-in-dark-mode treatment) */
.notif-avatar,
.examiner-opt-photo,
.l4-student-photo,
.live-test-avatar,
.d2-saved-tile-avatar,
.d2-exam-tbl-avatar,
.d2-exam-tbl-photo,
.client-mark,
.profile-photo {
  box-shadow: var(--avatar-ring);
}

/* Notifications */
.notif-wrap { position: relative; }
.notif-panel {
  position: absolute;
  top: calc(100% + 10px);
  inset-inline-end: -8px;
  width: 440px;
  max-width: calc(100vw - 24px);
  max-height: 560px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-lg);
  z-index: 150;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: dropdown-in 140ms ease-out;
}
.notif-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 14px 10px;
  border-bottom: 1px solid var(--line);
  background: var(--bg-sunken);
}
.notif-head-title {
  font-size: var(--fsz-h3);
  font-weight: 700;
  color: var(--ink);
}
.notif-head-counts {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  margin-top: 2px;
}
.notif-sep { color: var(--ink-4); padding: 0 2px; }
.notif-head-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}
.notif-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-label);
  color: var(--ink-2);
  cursor: pointer;
  user-select: none;
  padding: 4px 4px 4px 10px;
  border-radius: var(--r-pill);
  transition: background var(--t-fast);
}
[dir="rtl"] .notif-toggle { padding: 4px 10px 4px 4px; }
.notif-toggle:hover { background: var(--brand-tint); color: var(--ink); }
.notif-toggle input { display: none; }
.notif-switch {
  width: 32px; height: 18px;
  border-radius: var(--r-pill);
  background: var(--line-strong);
  position: relative;
  transition: background var(--t-fast);
  flex-shrink: 0;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 10%, transparent);
}
.notif-switch::after {
  content: '';
  position: absolute;
  top: 2px; inset-inline-start: 2px;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--bg-raised);
  box-shadow: 0 1px 2px rgba(0,0,0,0.18);
  transition: inset-inline-start 160ms cubic-bezier(.4,0,.2,1);
}
.notif-toggle input:checked + .notif-switch {
  background: var(--brand-500);
  box-shadow: inset 0 0 0 1px var(--brand-600);
}
.notif-toggle input:checked + .notif-switch::after { inset-inline-start: 16px; }

.notif-menu-wrap { position: relative; }
/* Notification kebab — md icon-only tier, matching `.icon-btn`
   siblings in the topnav. See tokens.css `--cta-h-*`. */
.notif-menu-btn { width: var(--cta-h-md); height: var(--cta-h-md); }
.notif-actions-menu {
  position: absolute;
  top: calc(100% + 4px);
  inset-inline-end: 0;
  min-width: 220px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  padding: 4px;
  z-index: 160;
  animation: dropdown-in 120ms ease-out;
}
.notif-actions-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-size: var(--fsz-label);
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  white-space: nowrap;
}
.notif-actions-item:hover { background: var(--brand-tint); }
.notif-actions-item svg { color: var(--ink-3); }

.notif-list {
  overflow-y: auto;
  overflow-x: hidden;
  flex: 1;
  scrollbar-gutter: stable;
}
.notif-empty {
  padding: 40px 20px;
  text-align: center;
  color: var(--ink-3);
  font-size: var(--fsz-label);
}
.notif-item {
  position: relative;
  padding: 12px 40px 12px 14px;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  cursor: pointer;
  transition: background var(--t-fast);
  overflow: hidden;
}
[dir="rtl"] .notif-item { padding: 12px 14px 12px 40px; }
.notif-item:hover { background: var(--brand-tint); }
.notif-item.is-new { background: color-mix(in srgb, var(--brand-500) 8%, var(--bg-raised)); }
.notif-item.is-new:hover { background: color-mix(in srgb, var(--brand-500) 14%, var(--bg-raised)); }

.notif-type {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-label);
  color: var(--ink-3);
}
.notif-type svg { color: var(--ink-3); flex-shrink: 0; }
.notif-type > span:first-of-type { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notif-new-pill {
  margin-inline-start: 4px;
  background: var(--brand-600);
  color: var(--brand-ink);
  font-size: var(--fsz-caption);
  font-weight: 600;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  letter-spacing: 0.02em;
  flex-shrink: 0;
}
.notif-body {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: 6px;
}
.notif-avatar {
  width: 26px; height: 26px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--bg-sunken);
  /* 1px ring comes from --avatar-ring (system rule above). */
  margin-top: 1px;
}
.notif-text {
  font-size: var(--fsz-body);
  line-height: 1.4;
  color: var(--ink);
  min-width: 0;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.notif-sender { font-weight: 600; }
.notif-text b { font-weight: 600; }
.notif-time {
  margin-top: 6px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}

/* Small unread dot, top-right. Hidden for read items unless hovered. */
.notif-dot {
  position: absolute;
  top: 12px;
  inset-inline-end: 12px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity 120ms ease;
}
.notif-dot::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: transparent;
  border: 1.5px solid var(--line-strong);
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.notif-item:hover .notif-dot { opacity: 1; }
.notif-dot.is-unread { opacity: 1; }
.notif-dot.is-unread::before {
  background: var(--brand-500);
  border-color: var(--brand-500);
}
.notif-dot:hover::before { transform: scale(1.2); border-color: var(--brand-500); }

/* Profile dropdown */
.profile-wrap { position: relative; }
.profile-menu {
  position: absolute;
  top: calc(100% + 8px);
  inset-inline-end: 0;
  min-width: 280px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-lg);
  padding: 12px 6px 6px;
  z-index: 120;
  animation: dropdown-in 120ms ease-out;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.profile-menu-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 9px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
}
.profile-menu-item:hover { background: var(--brand-tint); }
.profile-menu-item svg { color: var(--ink-3); }
.profile-menu-item.is-destructive { color: var(--err); }
.profile-menu-item.is-destructive svg { color: var(--err); }
.profile-menu-item.is-destructive:hover { background: color-mix(in oklab, var(--err) 10%, var(--bg-raised)); }
.profile-divider {
  height: 1px;
  background: var(--line);
  margin: 4px 2px;
}
.profile-section { padding: 6px 6px 4px; }
/* Language flag glyphs (English / Arabic letterforms inside a tab
   frame) are now inline SVGs rendered by the `Icon` component —
   see the `langEnglish` / `langArabic` entries in components.jsx.
   The previous `.lang-icon-*` mask-based system is gone. */
.profile-section-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: 6px;
  padding-inline-start: 4px;
}

/* ─────────── Client Rail — floating overlay ─────────────────────
   The institute / driving-school switcher. Anchored flush to the
   INSIDE edge of the left SideNav (not the viewport edge), so the
   SideNav stays visible and interactive while the rail is open.

   Position math:
     inset-block-start: 0                      — no topbar anymore
     inset-inline-start: var(--rail-offset)    — = sidenav width
   `--rail-offset` is set on `:root` and switches between
   `--sidenav-w-full` (256), `--sidenav-w-rail` (64, when collapsed
   or tablet auto-rail), and `0` (mobile, sidenav is a drawer that's
   closed by default).
   ───────────────────────────────────────────────────────────── */
.client-rail {
  position: fixed;
  inset-block-start: 0;
  bottom: 0;
  inset-inline-start: var(--rail-offset, var(--sidenav-w-full));
  width: 296px;
  max-width: 92vw;
  /* Above .rail-backdrop (z=46). SideNav uses default stacking, so
     the rail visually sits in front of any main-content overlay but
     does NOT cover the sidenav (different inline-start). */
  z-index: 47;
  background: var(--bg-raised);
  border-right: 1px solid var(--line);
  box-shadow: var(--sh-md);
  padding: 16px 12px;
  overflow-y: auto;
  overflow-x: hidden;
  transform: translateX(0);
  transition: transform var(--t-med), box-shadow var(--t-fast);
  pointer-events: auto;
}
[dir="rtl"] .client-rail { border-right: 0; border-left: 1px solid var(--line); }

.rail-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 4px 12px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--line);
}
.rail-title {
  font-size: var(--fsz-body); font-weight: 600; color: var(--ink);
}
.rail-close {
  width: 28px; height: 28px;
  display: grid; place-items: center;
  background: transparent; border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-3);
  cursor: pointer;
  padding: 0;
}
.rail-close:hover { background: var(--bg-sunken); color: var(--ink); }

.rail-head { padding: 4px 8px 12px; }
.rail-label {
  font-size: var(--fsz-caption);
  letter-spacing: 0.12em;
  color: var(--ink-4);
  font-weight: 600;
}
.rail-list { display: flex; flex-direction: column; gap: 4px; }

/* ── Branch-picker rail (Yard Live Tests) ───────────────────────
   Replaces the institute-only ClientRail on branch-scoped pages.
   Two-step picker:
     mode='branches'   → branches of current institute + CTA to switch
     mode='institutes' → flat institute list with back button
     mode='pick-inst'  → branches of a selected (but not yet active)
                         institute, with back to institutes list
   Reuses `.client-rail`, `.rail-header`, `.client-card` chrome —
   only the new pieces below are bespoke. */
/* Branch-picker header — two rows instead of squeezing back / title /
   close into a single line. Nav controls (back button + close) sit on
   row 1, title block (eyebrow + main name) gets the full width on row
   2. Without this the back label ("All institutes") wraps to two
   lines whenever the title is wide enough. */
.brp-rail .rail-header {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-rows: auto auto;
  gap: 8px 12px;
  align-items: center;
}
.brp-rail .rail-header .brp-back {
  grid-row: 1;
  grid-column: 1;
  justify-self: start;
  margin-inline-end: 0;
}
.brp-rail .rail-header .rail-close {
  grid-row: 1;
  grid-column: 2;
  justify-self: end;
}
.brp-rail .rail-header .rail-title {
  grid-row: 2;
  grid-column: 1 / -1;
}
.brp-rail .rail-title {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}
.brp-rail .rail-title-eyebrow {
  font-size: var(--fsz-caption);
  letter-spacing: 0.04em;
  color: var(--ink-3);
  font-weight: 500;
  text-transform: uppercase;
}
.brp-rail .rail-title-main {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Title block now owns its own row (full rail width), so no
     200px cap needed — let long institute names render in full. */
  max-inline-size: 100%;
}
/* Unified nav-link style — `.brp-back` is the single class used for
   header nav across all three modes:
   · mode 1: forward link to institutes view (chev-right + "All institutes")
   · mode 2: (no nav link — close + reopen returns to mode 1)
   · mode 3: back link to institutes view (chev-left + "All institutes")
   Direction is conveyed by chev placement: trailing chev = forward,
   leading chev = back. Single visual vocabulary in the rail-header. */
.brp-rail .brp-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--brand-700, var(--brand-500));
  font-size: var(--fsz-label);
  font-weight: 600;
  cursor: pointer;
  text-align: start;
  transition: background var(--t-fast), color var(--t-fast);
}
.brp-rail .brp-back:hover {
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
  color: var(--brand-800, var(--brand-700));
}
/* Branch row mark — small location icon instead of an institute logo.
   Branches share their institute's brand, but their own identity is
   geographic, so a pin icon reads more naturally than a colored chip. */
.brp-rail .brp-branch-card { padding-inline-start: 10px; }
.brp-rail .brp-branch-mark {
  width: 28px; height: 28px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  background: color-mix(in oklab, var(--brand-500) 8%, var(--bg-sunken));
  color: var(--brand-700, var(--brand-500));
}
.brp-rail .brp-hq-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* HQ marker — an INLINE badge beside the branch name (not a sub-line), so
   an HQ branch row stays the same height as the others. Sized to sit within
   the name's line height, adding no vertical height to the row. */
.brp-rail .brp-hq-tag {
  display: inline-block;
  margin-inline-start: 6px;
  padding: 0 6px;
  border-radius: var(--r-xs, 4px);
  font-size: var(--fsz-caption);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--brand-700);
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  vertical-align: baseline;
  flex-shrink: 0;   /* badge stays full-size; the name truncates instead */
}
/* Long branch names: the name truncates with an ellipsis while the HQ badge
   stays pinned at the end, and the row reserves space for the (absolute)
   favorite star so the name + badge never run under it. */
.brp-rail .brp-branch-card .client-body { min-width: 0; padding-inline-end: 22px; }
.brp-rail .brp-branch-card .client-name { display: flex; align-items: center; min-width: 0; }
/* Branch name is a `.cell-marquee` host (overflow/nowrap/fade-mask + the
   hover slide come from that shared system, driven by the global scanner +
   window.onNameHoverIn/Out — same as the saved-tests student name). We only
   add min-width:0 so it can shrink in the flex row and trigger the marquee.
   No text-overflow:ellipsis — that's incompatible with the sliding inner. */
.brp-rail .brp-branch-name-text { min-width: 0; }
/* Marquee names in the rail inherit the row's pointer cursor (shared
   .cell-marquee uses cursor:default for non-clickable cells) and drop any
   ellipsis (the fade-mask + hover-slide replace it — incompatible together). */
.brp-rail .cell-marquee { cursor: inherit; }
.brp-rail .client-name.cell-marquee { text-overflow: clip; }
/* Favorite indicator on a COLLAPSED institute row — a small gold star beside
   the branch count, so the institute holding the favorite branch is visible
   without expanding the accordion. Indicator only; the toggle stays on the
   branch row. Same gold as the branch favorite star. */
.brp-rail .brp-inst-fav {
  display: inline-flex;
  align-items: center;
  margin-inline-start: 5px;
  color: oklch(0.72 0.18 82);
}
[data-theme="dark"] .brp-rail .brp-inst-fav { color: oklch(0.82 0.17 85); }
/* Institute row — chevron at the trailing edge to signal "drills into
   branches" (vs. the branch row's terminal selection). */
.brp-rail .brp-inst-chev {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-3);
  flex-shrink: 0;
  padding: 0 4px;
}
.brp-rail .brp-inst-card.is-current {
  /* Subtle highlight on the institute the user is currently on (the
     one whose branches are shown by default). Not a "selected" state
     — just a visual breadcrumb. */
  background: color-mix(in oklab, var(--brand-500) 4%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 20%, var(--line));
}

/* ── Branch-picker accordion (Test page) ─────────────────────────────
   Single-page accordion: institutes listed, click one to expand its
   branches inline (one open at a time, like the system nav). Replaces
   the former 3-mode view-swapper. Shared by the normal + videowall views
   (one BranchPickerRail component). */
.brp-rail--accordion .rail-header--flat {
  display: flex;
  align-items: center;
  justify-content: space-between;
  grid-template-columns: none;
  grid-template-rows: none;
}
.brp-rail--accordion .rail-header--flat .rail-title { grid-row: auto; grid-column: auto; }
.brp-rail .brp-inst-group { display: flex; flex-direction: column; }
/* Open institute header gets a faint wash so it reads as one unit with
   its branches below; chevron picks up the brand tint when expanded. */
.brp-rail .brp-inst-group.is-expanded > .brp-inst-card {
  background: color-mix(in oklab, var(--brand-500) 5%, transparent);
}
.brp-rail .brp-inst-group.is-expanded .brp-inst-chev { color: var(--brand-600); }
/* Inline branch list under an institute — indented with a connecting
   rail so the nesting is obvious without a separate page/view. */
.brp-rail .brp-branch-sublist {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-inline-start: 19px;
  padding-inline-start: 10px;
  margin-block: 2px 6px;
  border-inline-start: 2px solid color-mix(in oklab, var(--brand-500) 16%, var(--line));
}
.brp-rail .brp-branch-sublist .brp-branch-card { padding-inline-start: 8px; }

.client-card {
  position: relative;
  width: 100%;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  text-align: start;
  transition: all var(--t-fast);
}
.client-card:hover {
  /* Same brand-500 @ 10% hover the sidenav rows use, so the
     institute switcher feels like an extension of the
     sidenav rather than a separate surface vocabulary. */
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
}
/* Active institute in the rail. Matches the SideNav active vocabulary:
   subtle brand-tinted pill background + brand-600 text + bold weight.
   The heavy border + inset-shadow combo the rail used to have is
   dropped — the full-height `.client-active-bar` (below) is now the
   primary "you're here" affordance, mirroring the strip on
   `.sidenav-item.is-active`. */
.client-card.is-active {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: transparent;
  box-shadow: none;
}
.client-card.is-active .client-name {
  color: var(--brand-600);
  font-weight: 600;
}
.client-card.is-active .client-sub { color: var(--brand-600); opacity: 0.85; }

/* Favorite star — hidden by default, visible on hover or when pinned */
.client-fav {
  position: absolute;
  top: 5px;
  inset-inline-end: 5px;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  background: transparent;
  border: 0;
  border-radius: 50%;
  color: var(--ink-4);
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
  padding: 0;
  z-index: 2;
}
.client-card:hover .client-fav,
.client-card:focus-within .client-fav { opacity: 1; }
.client-fav:hover { background: var(--bg-sunken); color: oklch(0.72 0.18 82); }
.client-fav.is-on {
  opacity: 1;
  color: oklch(0.72 0.18 82);
}
.client-fav.is-on:hover { color: oklch(0.66 0.19 82); }
[data-theme="dark"] .client-fav.is-on { color: oklch(0.82 0.17 85); }

.client-mark {
  width: 40px; height: 40px;
  flex-shrink: 0;
  border-radius: 11px;
  display: grid; place-items: center;
  font-size: var(--fsz-label);
  font-weight: 700;
  letter-spacing: .02em;
  color: white;
  position: relative;
  overflow: hidden;
}
.client-mark::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(255,255,255,0.18), rgba(0,0,0,0.08));
}
.client-mark > span { position: relative; z-index: 1; }

/* Tonal mark backgrounds — derived from each client's brand for variety */
.client-mark[data-tone="A"] { background: oklch(0.55 0.16 240); }
.client-mark[data-tone="B"] { background: oklch(0.60 0.17 18); }
.client-mark[data-tone="C"] { background: oklch(0.55 0.13 160); }
.client-mark[data-tone="D"] { background: oklch(0.50 0.16 285); }
.client-mark[data-tone="E"] { background: oklch(0.55 0.14 205); }
.client-mark[data-tone="F"] { background: oklch(0.65 0.14 45); }
.client-mark[data-tone="G"] { background: oklch(0.58 0.14 325); }
.client-mark[data-tone="H"] { background: oklch(0.55 0.12 185); }
.client-mark[data-tone="I"] { background: oklch(0.55 0.11 120); }
.client-mark[data-tone="J"] { background: oklch(0.55 0.16 255); }
.client-mark[data-tone="K"] { background: oklch(0.45 0.07 35); }

/* Logo variant — when a real school icon SVG is rendered, drop the
   tone background so the brand colors aren't tinted, and give the
   tile a neutral surface with a hairline border. The `.client-mark`
   selector is doubled to outweigh the `.client-mark[data-tone="X"]`
   rules above (same specificity → later rule wins). */
.client-mark.client-mark--logo {
  background: #fff;
  border: 1px solid oklch(0.92 0.005 250);
}
.client-mark.client-mark--logo::after { display: none; }
.client-mark.client-mark--logo > img {
  width: 100%; height: 100%;
  object-fit: contain;
  padding: 4px;
  display: block;
}
[data-theme="dark"] .client-mark.client-mark--logo {
  background: oklch(0.97 0.005 250);
  border-color: oklch(0.30 0.02 250);
}

.client-body {
  min-width: 0;
  flex: 1;
  display: flex; flex-direction: column;
  gap: 1px;
}
.client-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.3;
}
.client-sub {
  font-size: var(--fsz-caption);
  letter-spacing: 0;
  color: var(--ink-3);
  font-weight: 500;
  margin-top: 2px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
/* Pin icon (location glyph for "N branches"). Resting state is
   --ink-4 grey so inactive cards stay quiet. Active-card override
   below flips it to --brand-600 so the pin matches the brand-tinted
   "N branches" text in the active card. */
.client-sub > svg { color: var(--ink-4); flex-shrink: 0; }
.client-card.is-active .client-sub > svg { color: var(--brand-600); }
/* Full-height active strip inside the active rail card. Same
   3px brand-500 strip vocabulary used on `.sidenav-item.is-active`
   so both surfaces share an identical "you are here" affordance.
   Sits flush against the card's inside inline-start edge; the
   card has `border-radius: var(--r-md)`, so the strip's leading
   corners match it (rounded on the leading side, flat on the
   trailing side — like a bookmark tab). */
.client-active-bar {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--brand-500);
  border-radius: var(--r-md) 0 0 var(--r-md);
  pointer-events: none;
}
[dir="rtl"] .client-active-bar {
  border-radius: 0 var(--r-md) var(--r-md) 0;
}

/* ─────────── Main column ─────────── */
.main {
  padding: 20px 16px 40px;
  display: flex; flex-direction: column;
  gap: 12px;
  min-width: 0;
  /* Page canvas: a neutral sunken surface with a whisper of brand so the
     cards on top (topnav, table, filter-bar — all on --bg-raised) read as
     elevated. Keeps the brand color reserved for interactive states. */
  background: color-mix(in srgb, var(--brand-500) 1%, var(--bg-sunken));
}
/* Yard Live (and any page using the .ylt-split split layout) — drop
   main's 40px bottom padding because the split's two columns are
   self-contained panels that already carry their own internal padding.
   Without this override the global main { padding-bottom: 40px } shows
   as an empty white strip below the tables/canvas, breaking the
   "panel fills viewport" feel of the live page. Scoped via :has() so
   other pages keep their normal bottom padding. */
.main:has(.ylt-split) {
  padding-bottom: 0;
}
[data-theme="dark"] .main {
  background: color-mix(in srgb, var(--brand-500) 2%, var(--bg-sunken));
}
/* Cap content children at a sensible max-width on ultra-wide monitors so
   tables/cards don't stretch awkwardly. The .main background still spans
   edge-to-edge; only the content children are centered + capped. Below
   1600px the cap has no effect — content fills the available width. */
.main > * {
  width: 100%;
  max-width: 1600px;
  align-self: center;
}
.page-title {
  margin: 0;
  font-size: var(--fsz-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.2;
}

/* ─────────── FILTER BAR ─────────── */
.filter-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-xs);
  /* Allow children to wrap; takes effect only when the @container
     rule below forces a flex-basis of 100% on .filter-pills. At
     wider widths everything still fits on a single row. */
  flex-wrap: wrap;
  /* Make the bar an inline-size container so the rule below can
     read its own width — needed because the bar gets squeezed by
     the sidenav at desktop widths where a viewport media query
     wouldn't fire. */
  container-type: inline-size;
}
/* Below ~880px filter-bar width the pills can't fit beside
   search + density without stack-wrapping awkwardly (search left,
   pills cascading down on the right in 2-3 messy rows). At this
   threshold push the pills to their own full-width row underneath
   so they read as one tidy group instead of a fragmented stack.
   Threshold derived from measured content: search 256 + 4×pill
   gaps + 5 pills (sum ~540) + density 32 + outer gaps ≈ 867,
   plus a small headroom buffer. Mirrors the existing ≤720
   viewport layout but container-based, so it correctly handles
   the sidenav-open case at any viewport. */
@container (max-width: 879px) {
  /* Selectors carry the parent `.filter-bar` class so they beat the
     base `.filter-pills`/`.filter-search` rules that appear later in
     the cascade — without this bump the @container's `flex: 1 1 100%`
     loses to the later same-specificity base rule. */
  .filter-bar .filter-search { flex: 1 1 0; order: 0; min-width: 0; }
  .filter-bar .filter-actions { order: 1; margin-inline-start: auto; flex-shrink: 0; }
  .filter-bar .filter-pills {
    order: 2;
    flex: 1 1 100%;
    flex-wrap: wrap;
    overflow: visible;
  }
}
/* Mobile (≤720): search + density share row 1, pills span row 2.
   Aligned with the canonical mobile breakpoint — same boundary as
   the rest of the mobile chrome (drawer, table h-scroll, etc.). */
@media (max-width: 720px) {
  .filter-bar {
    flex-wrap: wrap;
    padding: 8px;
  }
  .filter-bar .filter-search { flex: 1 1 0; order: 0; min-width: 0; }
  .filter-bar .filter-actions { order: 1; margin-inline-start: auto; flex-shrink: 0; }
  .filter-bar .filter-pills {
    order: 2;
    flex: 1 1 100%;
    flex-wrap: wrap;
    overflow: visible;
  }
  /* Custom-Reports tpl variant: search gets its own full-width row
     above the pills (different from the savedtests bar where search
     shares row 1 with density). The chained-class specificity here
     wins over the base `.filter-bar--reports { flex-wrap: nowrap }`
     declared elsewhere. */
  .filter-bar--reports.tpl-filter-bar { flex-wrap: wrap; }
  .filter-bar--reports.tpl-filter-bar .filter-search { flex: 1 1 100%; }
  .filter-bar--reports.tpl-filter-bar .filter-pills {
    flex: 1 1 100%;
    flex-wrap: wrap;
  }
}

.filter-search {
  display: flex; align-items: center; gap: 8px;
  background: var(--bg-sunken);
  border-radius: var(--r-md);
  padding: 7px 10px;
  /* Trimmed 24px (was 280) so the pills get more horizontal room
     on the same row before they have to wrap or shrink labels. */
  flex: 0 0 256px;
  color: var(--ink-3);
  transition: all var(--t-fast);
}
.filter-search:focus-within {
  /* Aligned to the system's new hover/active vocabulary —
     soft brand-tinted glow instead of the prior heavy `--ring`
     token (brand @ 40% / 55%). The 1px inner border at 35%
     brand gives the input a defined edge, and the 3px outer
     halo at 10% brand matches the hover pill saturation. */
  background: var(--bg-raised);
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--brand-500) 35%, var(--line)),
    0 0 0 4px color-mix(in oklab, var(--brand-600) 6%, transparent);
}
.filter-search input {
  border: 0; background: transparent; outline: 0;
  flex: 1; min-width: 0;
  font-size: var(--fsz-body);
  color: var(--ink);
  font-family: inherit;
}
.filter-search input::placeholder { color: var(--ink-3); }
.kbd {
  font-size: var(--fsz-caption);
  padding: 2px 5px;
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--ink-3);
  border: 1px solid var(--line);
  letter-spacing: 0;
}

.filter-pills {
  display: flex; align-items: center; gap: 6px;
  flex: 1; min-width: 0;
  flex-wrap: wrap;
}
.filter-pill-wrap { position: relative; flex-shrink: 0; }
.filter-pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 11px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--ink-2);
  font-size: var(--fsz-label);
  font-weight: 500;
  cursor: pointer;
  transition: all var(--t-fast);
  white-space: nowrap;
  flex-shrink: 0;
  font-family: inherit;
  max-width: 220px;
}
/* Filter pill hover + active aligned to the system vocabulary:
   hover  → brand-500 @ 10% transparent (the "preview" tier)
   active → brand-500 @ 12% transparent (the "statement" tier),
            brand-600 text, lighter brand-tinted border so the
            active pill reads as a single coherent surface
   `is-open` (the pill's dropdown is open) keeps the hover tier
   so the trigger looks "armed" without claiming active status. */
.filter-pill:hover {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 20%, var(--line));
  color: var(--ink);
}
/* Dark-mode contrast boost — the default `--bg-raised` (0.23) sits
   only a hair above the page surface (`--bg-base` ~0.2), which made
   the resting filter pills and search input visually melt into the
   background. Lighten the pill / search chrome a notch and bump the
   border so the row reads as a defined input cluster instead of a
   dim wash. Same idea applies to the kbd hint inside the search. */
[data-theme="dark"] .filter-pill,
[data-theme="dark"] .filter-search,
[data-theme="dark"] .srf-menu-btn,
[data-theme="dark"] .cols-menu-btn,
[data-theme="dark"] .density-icon-btn,
[data-theme="dark"] .lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn,
[data-theme="dark"] .ylc-action--more {
  /* Uplift targets bumped from 0.35→0.42 (resting bg) and 0.4→0.46
     (border) after the May 2026 Option-B surface lift raised
     `--bg-raised` from 0.245 to 0.295. The earlier targets sat too
     close to the new surface; new targets restore the ~0.04 delta
     so the CTAs read as defined affordances above the page. */
  background: color-mix(in oklab, var(--bg-raised) 70%, oklch(0.42 0.010 var(--brand-h)));
  border-color: color-mix(in oklab, var(--line) 60%, oklch(0.46 0.014 var(--brand-h)));
}
[data-theme="dark"] .filter-search:focus-within {
  background: color-mix(in oklab, var(--bg-raised) 60%, oklch(0.44 0.012 var(--brand-h)));
}
[data-theme="dark"] .filter-pill:hover,
[data-theme="dark"] .srf-menu-btn:hover,
[data-theme="dark"] .cols-menu-btn:hover,
[data-theme="dark"] .density-icon-btn:hover,
[data-theme="dark"] .lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn:hover,
[data-theme="dark"] .ylc-action--more:hover {
  background: color-mix(in oklab, var(--brand-500) 14%, oklch(0.38 0.010 var(--brand-h)));
  border-color: color-mix(in oklab, var(--brand-500) 35%, var(--line));
}
.filter-pill.is-active {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 22%, var(--line));
  color: var(--brand-600);
  font-weight: 600;
}
[data-theme="dark"] .filter-pill.is-active { color: var(--brand-600); }
.filter-pill.is-open {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 20%, var(--line));
}
.filter-pill.is-active.is-open {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
}
.filter-pill svg { opacity: 0.7; flex-shrink: 0; }
.filter-pill-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* Optional axis label that prefixes the pill value (e.g. "Last run"
   in front of "This week"). Sits between the leading icon and the
   value, separated by an em-dot, in muted ink so the value still
   reads as the dominant piece. */
.filter-pill-prefix {
  color: var(--ink-3);
  white-space: nowrap;
  flex-shrink: 0;
}
.filter-pill-prefix::after {
  content: ' · ';
  color: var(--ink-4);
}
/* Time filter pill — when a custom range is shown the label is much longer
   (e.g. "1 Mar 2026 → 24 Apr 2026" / Arabic month names), so allow it to
   grow to fit the full text rather than truncate. */
.filter-pill.filter-pill--wide { max-width: none; }
.filter-pill.filter-pill--wide .filter-pill-label { overflow: visible; text-overflow: clip; }

/* Standard filter Clear button — used on every page that has a
   filter bar (Saved Tests, Reports, Custom Reports). Inline-text
   style, no border, subtle underline. Picked because it doesn't
   compete with the actual filter pills (which carry the heavier
   visual weight) — Clear is a destructive secondary action and the
   light treatment keeps it discoverable without making it loud. */
.filter-reset {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  background: transparent;
  border: 0;
  color: var(--ink-3);
  font-size: var(--fsz-label);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
  flex-shrink: 0;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in oklab, var(--ink-3) 50%, transparent);
  transition: color var(--t-fast), text-decoration-color var(--t-fast);
}
.filter-reset:hover {
  color: var(--err);
  text-decoration-color: var(--err);
}
/* `.filter-reset--inline` kept as a no-op alias — older call sites
   pass it explicitly. The base class is now the inline style. */
.filter-reset--inline { /* no-op; merged into .filter-reset */ }

.filter-search-clear {
  background: transparent;
  border: 0;
  padding: 2px;
  cursor: pointer;
  color: var(--ink-3);
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
}
.filter-search-clear:hover { color: var(--ink); background: var(--bg-raised); }
.filter-search-clear.is-hidden { visibility: hidden; pointer-events: none; }

/* Filter dropdown menu */
.filter-menu {
  position: absolute;
  top: calc(100% + 6px);
  inset-inline-start: 0;
  z-index: 40;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  min-width: 200px;
  max-width: 280px;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px;
  animation: dropdown-in 120ms ease-out;
}
@keyframes dropdown-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
.filter-menu-item {
  display: flex; align-items: center; justify-content: flex-start; gap: 8px;
  width: 100%;
  padding: 7px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-size: var(--fsz-label);
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  white-space: nowrap;
}
.filter-menu-item > span:not(.multi-check):not(.examiner-opt) { flex: 1; }
.filter-menu-item:hover { background: var(--brand-tint); }
/* Keyboard-focused option (arrow-key navigation). Same visual as
   :hover so mouse + keyboard share affordance. The active row also
   shows a brand-colored inset ring on the start edge so the user
   can tell which row Enter will activate. */
/* Keyboard-active row — same brand-tinted background as `:hover`.
   Used to carry a 2px brand-colored inset bar on the leading edge
   as a "this row commits on Enter" affordance, but `onMouseEnter`
   syncs `activeIdx` (so mouse and keyboard navigation share state),
   which meant the bar also rendered on plain hover and looked like
   a stray strip on the left of every hovered item. Background tint
   alone is enough — for keyboard-only users the highlight follows
   the arrow keys; for mouse users it follows hover. The class stays
   on the element as a hook for future per-mode behavior. */
.filter-menu-item.is-kbd-active {
  background: var(--brand-tint);
}
.filter-menu-item.is-on {
  color: var(--brand-700);
  background: var(--brand-tint);
}
[data-theme="dark"] .filter-menu-item.is-on { color: var(--brand-700); }
.filter-menu-item svg { color: var(--brand-600); opacity: 1; }

/* Multi-select rows: checkbox replaces brand-tint background */
.filter-menu-item.is-multi { color: var(--ink); background: transparent; }
.filter-menu-item.is-multi:hover { background: var(--brand-tint); }
.filter-menu-item.is-multi.is-on { color: var(--ink); background: transparent; }
.filter-menu-item.is-multi.is-on:hover { background: var(--brand-tint); }
.multi-check {
  width: 16px; height: 16px;
  border-radius: 4px;
  border: 1px solid var(--line-strong);
  background: var(--bg-raised);
  display: inline-grid; place-items: center;
  flex-shrink: 0;
  color: var(--brand-ink);
  transition: all var(--t-fast);
}
.multi-check.is-on {
  background: var(--brand-600);
  border-color: var(--brand-600);
}
.multi-check svg { color: inherit; opacity: 1; }
.multi-check.is-on svg { color: var(--brand-ink); }

.filter-menu-search {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  margin: 2px 2px 6px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-3);
}
.filter-menu-search:focus-within {
  /* Same soft brand-tinted halo as .filter-search above, scaled
     down to a 3px glow for the smaller in-menu search input. */
  border-color: color-mix(in oklab, var(--brand-500) 35%, var(--line));
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-600) 6%, transparent);
}
.filter-menu-search input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  outline: 0;
  font: inherit;
  font-size: var(--fsz-label);
  color: var(--ink);
}
.filter-menu-search input::placeholder { color: var(--ink-3); }
.filter-menu-empty {
  padding: 14px 10px;
  text-align: center;
  font-size: var(--fsz-label);
  color: var(--ink-3);
}

.filter-menu-clear {
  width: 100%;
  padding: 6px 10px;
  background: transparent;
  border: 0;
  text-align: start;
  font-family: inherit;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  cursor: pointer;
  border-bottom: 1px solid var(--line);
  margin-bottom: 4px;
  border-radius: 0;
}
.filter-menu-clear:hover { color: var(--err); background: var(--bg-sunken); }

/* Examiner option with photo */
.examiner-opt {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.examiner-opt-photo {
  width: 22px; height: 22px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  flex-shrink: 0;
}

/* RecipientsPicker rows — 22px circular icon-mark + name + role/count
   subtitle. The mark is brand-tinted for roles (groups) and neutral
   for individual users, so the user can scan kind at a glance
   without reading every label. */
.recipient-opt {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.recipient-opt-mark {
  width: 22px; height: 22px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border: 1px solid var(--line);
}
.recipient-opt-mark.is-user {
  background: var(--bg-sunken);
  color: var(--ink-3);
}
.recipient-opt-mark.is-role {
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line));
  color: var(--brand-700);
}
.recipient-opt-text {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
}
.recipient-opt-name {
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.recipient-opt-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Time filter menu — presets + custom range */
.time-menu { min-width: 260px; }
/* Calendar footer hint + error (still used by the calendar's footer) */
.custom-range-error { font-size: var(--fsz-caption); color: var(--err); }
.custom-range-hint { font-size: var(--fsz-label); color: var(--ink-3); }
.cal-actions .custom-range-hint { font-size: var(--fsz-label); }
/* Removed RTL bumps to body (15px): natural --fsz-label RTL bump
   to 13px is sufficient and keeps hint in the label tier. */

/* ── Time filter dropdown: presets list (default) + optional calendar pane ──
   The presets rail keeps the SAME size in both list mode (default) and combo
   mode (when the calendar is showing), so the sidebar doesn't visibly
   resize when the user opens the calendar. */
.time-menu .combo-presets {
  display: flex; flex-direction: column;
  gap: 2px;
}
.combo-preset-item {
  display: flex; align-items: center; justify-content: space-between;
  gap: 6px;
  width: 100%;
  padding: 7px 9px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-size: var(--fsz-label);
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  white-space: nowrap;
}
.combo-preset-item:hover { background: var(--brand-tint); }
.combo-preset-item.is-on { background: var(--brand-tint); color: var(--brand-700); font-weight: 600; }
[data-theme="dark"] .combo-preset-item.is-on { color: var(--brand-700); }
.combo-preset-item svg { color: var(--brand-600); }
.combo-preset-item.is-custom > svg:last-child { color: var(--ink-3); opacity: 0.7; }
[dir="rtl"] .combo-preset-item { font-size: var(--fsz-body); }

/* List mode (no calendar) — narrow dropdown matching the combo sidebar width */
.time-menu:not(.time-menu--combo) {
  width: 168px;
  min-width: 168px;
  max-width: 168px;
  max-height: none;
  padding: 6px;
  overflow: visible;
}

/* Combo mode (calendar visible) — split layout */
.time-menu--combo {
  width: 600px;
  min-width: 600px;
  max-width: none;
  max-height: none;
  padding: 0;
  display: grid;
  grid-template-columns: 168px 1fr;
  overflow: visible;
}
.time-menu--combo .combo-presets {
  padding: 6px;
  border-inline-end: 1px solid var(--line);
  background: transparent;
}

.combo-calendar {
  display: flex; flex-direction: column;
  background: var(--bg-raised);
}
.cal-nav-btn {
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-2); cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.cal-nav-btn:hover:not(:disabled) { background: var(--bg-hover); color: var(--ink); border-color: var(--line-strong); }
.cal-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.cal-nav-spacer { width: 24px; height: 24px; flex-shrink: 0; }

.cal-body {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 0;
}
.cal-month { padding: 8px 10px 10px; }
.cal-month + .cal-month { border-inline-start: 1px solid var(--line); }
.cal-month-title {
  display: flex; align-items: center; justify-content: space-between;
  gap: 6px;
  margin-bottom: 6px;
}
.cal-month-label {
  flex: 1;
  text-align: center;
  font-size: var(--fsz-label); font-weight: 600; color: var(--ink);
  letter-spacing: 0.01em;
}
[dir="rtl"] .cal-month-label { font-size: var(--fsz-body); letter-spacing: 0; }

.cal-weekdays {
  display: grid; grid-template-columns: repeat(7, 1fr);
  gap: 0;
  margin-bottom: 2px;
}
.cal-weekday {
  text-align: center;
  font-size: var(--fsz-chart-label); font-weight: 600;
  color: var(--ink-4);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 0;
}
[dir="rtl"] .cal-weekday { font-size: var(--fsz-caption); letter-spacing: 0; }

.cal-grid {
  display: grid; grid-template-columns: repeat(7, 1fr);
  gap: 0;
}
.cal-grid .cal-day {
  position: relative;
  height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0;
  font-family: inherit;
  font-size: var(--fsz-caption);
  color: var(--ink);
  cursor: pointer;
  padding: 0;
  border-radius: 0;
  overflow: visible;
}
.cal-grid .cal-day > span {
  position: relative; z-index: 2;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  transition: background-color 80ms ease, color 80ms ease;
}
.cal-grid .cal-day.is-blank { pointer-events: none; cursor: default; }
.cal-grid .cal-day.is-disabled { color: var(--ink-4); opacity: 0.45; cursor: not-allowed; }

/* Hover for non-selected days — show a soft pill on the inner span */
.cal-grid .cal-day:not(.is-disabled):not(.is-blank):not(.in-band):hover > span {
  background: var(--brand-tint);
  color: var(--ink);
}

/* Today indicator — brand-colored ring on the inner pill (when not selected endpoint) */
.cal-grid .cal-day.is-today:not(.is-start):not(.is-end) > span {
  box-shadow: inset 0 0 0 1.5px var(--brand);
  color: var(--brand-700);
  font-weight: 600;
}

/* ── Range band ──
   Painted via a ::before that fills the cell horizontally. We toggle width
   per cell-class so the band visually flows across the row, with rounded
   ends only at the actual range edges or row boundaries. */
.cal-grid .cal-day.in-band::before {
  content: '';
  position: absolute;
  top: 2px; bottom: 2px;
  left: 0; right: 0;
  background: var(--brand-tint);
  z-index: 1;
  pointer-events: none;
}
[data-theme="dark"] .cal-grid .cal-day.in-band::before {
  background: color-mix(in srgb, var(--brand) 22%, transparent);
}
/* Half-bands for endpoints when there is a range */
.cal-grid .cal-day.band-trail-half::before { left: 50%; right: 0; }
[dir="rtl"] .cal-grid .cal-day.band-trail-half::before { left: 0; right: 50%; }
.cal-grid .cal-day.band-lead-half::before { left: 0; right: 50%; }
[dir="rtl"] .cal-grid .cal-day.band-lead-half::before { left: 50%; right: 0; }
/* Single-point selection has no band */
.cal-grid .cal-day.is-single-point::before { display: none; }

/* Row-edge rounding on the band */
.cal-grid .cal-day.round-lead::before {
  border-start-start-radius: 14px;
  border-end-start-radius: 14px;
}
.cal-grid .cal-day.round-trail::before {
  border-start-end-radius: 14px;
  border-end-end-radius: 14px;
}

/* Endpoints — full-cell CTA-style pill on top of the band */
.cal-grid .cal-day.is-start > span,
.cal-grid .cal-day.is-end > span {
  position: absolute;
  inset: 2px 3px;
  width: auto; height: auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--brand-600);
  color: var(--brand-ink);
  font-weight: 700;
  font-size: var(--fsz-caption);
  border-radius: 11px;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--brand-600) 28%, transparent);
  z-index: 3;
}
.cal-grid .cal-day.is-start:hover > span,
.cal-grid .cal-day.is-end:hover > span {
  background: var(--brand-700, var(--brand-600));
}

/* In-range mids: keep base text dark/legible on tint */
.cal-grid .cal-day.is-mid { color: var(--ink); }

/* Calendar footer */
.cal-footer {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  border-top: 1px solid var(--line);
  background: var(--bg-sunken);
}
.cal-summary {
  font-size: var(--fsz-label);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  display: flex; flex-direction: column; gap: 2px;
}
.cal-summary-hint { color: var(--ink-3); font-style: italic; font-size: var(--fsz-caption); }
.cal-summary-strong { color: var(--ink); font-weight: 600; }
[dir="rtl"] .cal-summary { font-size: var(--fsz-label); }
[dir="rtl"] .cal-summary-hint { font-style: normal; font-size: var(--fsz-label); }
.cal-actions { display: flex; align-items: center; gap: 8px; }

/* Combo responsive
   - under 660: presets become a horizontal pill bar above the calendar (still two months side-by-side if width allows)
   - under 520: single-month calendar
   - under 420: tighten footer to two rows */
@media (max-width: 720px) {
  .time-menu--combo {
    width: calc(100vw - 24px);
    min-width: 0;
    max-width: 560px;
    grid-template-columns: 1fr;
  }
  .time-menu--combo .combo-presets {
    flex-direction: row; flex-wrap: wrap;
    border-inline-end: 0;
    border-bottom: 1px solid var(--line);
    padding: 6px;
    gap: 4px;
  }
  .time-menu--combo .combo-preset-item {
    flex: 1 1 auto; justify-content: center;
    padding: 6px 10px;
  }
  .time-menu--combo .combo-preset-item.is-custom > svg:last-child { display: none; }
}
@media (max-width: 480px) {
  .cal-body { grid-template-columns: 1fr; }
  .cal-month + .cal-month { border-inline-start: 0; border-top: 1px solid var(--line); }
}
@media (max-width: 480px) {
  .cal-footer {
    flex-wrap: wrap;
    align-items: stretch;
  }
  .cal-actions {
    width: 100%;
    justify-content: space-between;
  }

  /* Bottom-sheet style on tiny viewports (e.g. 320–375 phones).
     The JS positioning otherwise anchors the popover to the pill's
     bottom-edge, which on a narrow phone pushes the calendar +
     footer past the viewport bottom and hides the Apply button.
     Anchoring the menu to the viewport bottom and letting the
     calendar body scroll internally keeps Apply always visible
     and reachable. `!important` is needed to override the inline
     `top/left` set by the React popover-positioning effect. */
  .time-menu--combo {
    position: fixed !important;
    top: auto !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    min-width: 0 !important;
    max-height: 92vh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    display: flex;
    flex-direction: column;
    grid-template-columns: none;
    overflow: hidden;
  }
  .time-menu--combo .combo-presets { flex-shrink: 0; }
  .time-menu--combo .combo-calendar {
    flex: 1;
    min-height: 0;
  }
  /* Calendar body becomes the internal scroll container so the
     footer (Apply + summary) stays pinned to the sheet's bottom
     edge regardless of how tall the month grid is. */
  .time-menu--combo .cal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }
  .time-menu--combo .cal-footer {
    flex-shrink: 0;
    box-shadow: 0 -1px 0 var(--line);
  }
}

.filter-actions {
  display: flex; align-items: center; gap: 6px;
}

/* Density icon toggle — compact one-click switch between comfort/dense */
.density-icon-btn {
  /* md icon-only tier — see tokens.css `--cta-h-*`. */
  width: var(--cta-h-md); height: var(--cta-h-md);
  display: inline-grid; place-items: center;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  padding: 0;
  transition: all var(--t-fast);
  flex-shrink: 0;
}
.density-icon-btn:hover {
  /* Aligned to the system hover vocabulary — soft brand tint (6%
     brand-600 transparent via --brand-tint). The previous neutral
     grey treatment (--bg-sunken + --line-strong) predates the
     token unification and read as flat / off-system next to other
     icon buttons in the same filter bar that now use --brand-tint.
     Border stays --line (resting) — the brand-tinted bg already
     differentiates the hover state, no edge color shift needed. */
  background: var(--brand-tint);
  color: var(--ink);
}

/* Columns visibility menu — sits in .filter-actions next to the
   density toggle. Button matches the density-icon-btn shape; the
   popover anchors to its trailing edge and opens below. */
.cols-menu { position: relative; flex-shrink: 0; }
.cols-menu-btn {
  /* `position: relative` anchors the .cols-menu-dot indicator.
     md icon-only tier — see tokens.css `--cta-h-*`. */
  position: relative;
  width: var(--cta-h-md); height: var(--cta-h-md);
  display: inline-grid; place-items: center;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  padding: 0;
  transition: all var(--t-fast);
}
.cols-menu-btn:hover,
.cols-menu-btn.is-open {
  background: var(--brand-tint);
  color: var(--ink);
}
.cols-menu-btn.is-open { color: var(--brand-700); }
/* Active state — when at least one optional column is hidden, the
   trigger reads as "currently filtering" and adopts the same
   visual vocabulary as `.filter-pill.is-active`: 12% brand-tint
   background, brand-tinted border, brand text color. The dot
   indicator (rendered alongside) reinforces it but the bg shift
   makes the customisation legible even before noticing the dot. */
.cols-menu-btn.has-hidden {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 22%, var(--line));
  color: var(--brand-600);
}
.cols-menu-btn.has-hidden:hover,
.cols-menu-btn.has-hidden.is-open {
  background: color-mix(in oklab, var(--brand-500) 18%, transparent);
  color: var(--brand-700);
}
/* Indicator dot — shown when at least one optional column is
   hidden. Tells the user at a glance that the table view is
   customised from default; tooltip on the trigger spells out
   the count. */
.cols-menu-dot {
  position: absolute;
  top: 5px;
  inset-inline-end: 5px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--brand-600);
  box-shadow: 0 0 0 2px var(--bg-raised);
  pointer-events: none;
}
.cols-menu-btn:hover .cols-menu-dot,
.cols-menu-btn.is-open .cols-menu-dot {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand-tint) 100%, var(--bg-raised));
}
.cols-menu-pop {
  position: absolute;
  top: calc(100% + 6px);
  inset-inline-end: 0;
  inset-inline-start: auto;
  min-inline-size: 200px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  padding: 6px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cols-menu-head {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 6px 8px 4px;
}
.cols-menu-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-block-size: 320px;
  overflow-y: auto;
}
.cols-menu-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fsz-body);
  color: var(--ink);
  user-select: none;
}
.cols-menu-item:hover { background: var(--brand-tint); }
.cols-menu-item input[type="checkbox"] {
  accent-color: var(--brand-600);
  flex-shrink: 0;
  cursor: pointer;
}
/* Required (locked) rows — Traffic file / Date / Student. The
   checkbox is disabled-checked and visually muted so the user
   can see WHAT'S always on without being able to uncheck it.
   `title` (set in JSX) carries the "Always visible" tooltip. */
.cols-menu-item.is-required {
  color: var(--ink-3);
  cursor: not-allowed;
}
.cols-menu-item.is-required:hover { background: transparent; }
.cols-menu-item.is-required input[type="checkbox"] {
  cursor: not-allowed;
  opacity: 0.55;
}
.cols-menu-reset {
  margin-block-start: 4px;
  padding: 7px 8px;
  background: transparent;
  border: 0;
  border-block-start: 1px solid var(--line);
  border-radius: 0;
  color: var(--brand-700);
  font-size: var(--fsz-body);
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  text-align: start;
  transition: background var(--t-fast);
}
.cols-menu-reset:hover { background: var(--brand-tint); }

/* StatusResultMenu — combined Status + Result filter icon-CTA used
   on Live Tests. Same dimensions and interaction vocabulary as
   `.cols-menu` so the toolbar row reads as a coherent action
   cluster (filter / columns / density). The trailing-edge badge
   shows the active filter count (instead of the cols-menu's
   single dot) — gives a clearer "how filtered are you" signal at
   a glance. */
.srf-menu { position: relative; flex-shrink: 0; }
.srf-menu-btn {
  /* md icon-only tier — see tokens.css `--cta-h-*`. */
  position: relative;
  width: var(--cta-h-md); height: var(--cta-h-md);
  display: inline-grid; place-items: center;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  padding: 0;
  transition: all var(--t-fast);
}
.srf-menu-btn:hover,
.srf-menu-btn.is-open {
  background: var(--brand-tint);
  color: var(--ink);
}
.srf-menu-btn.is-open { color: var(--brand-700); }
.srf-menu-btn.has-active {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 22%, var(--line));
  color: var(--brand-600);
}
.srf-menu-btn.has-active:hover,
.srf-menu-btn.has-active.is-open {
  background: color-mix(in oklab, var(--brand-500) 18%, transparent);
  color: var(--brand-700);
}
/* Active-count badge — small brand-tinted pill anchored to the
   trailing-top corner, showing the number of currently-applied
   filters. Outline ring (box-shadow) keeps it readable when it
   overlaps the button border. */
.srf-menu-count {
  position: absolute;
  inset-block-start: -4px;
  inset-inline-end: -4px;
  min-inline-size: 16px;
  block-size: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--brand-600);
  color: var(--brand-ink);
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--bg-raised);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
.srf-menu-pop {
  position: absolute;
  top: calc(100% + 6px);
  inset-inline-end: 0;
  inset-inline-start: auto;
  min-inline-size: 200px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  padding: 6px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.srf-menu-head {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 6px 8px 4px;
}
.srf-menu-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.srf-menu-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fsz-body);
  color: var(--ink);
  user-select: none;
}
.srf-menu-item:hover { background: var(--brand-tint); }
.srf-menu-item input[type="checkbox"] {
  accent-color: var(--brand-600);
  flex-shrink: 0;
  cursor: pointer;
}
.srf-menu-reset {
  margin-block-start: 4px;
  padding: 7px 8px;
  background: transparent;
  border: 0;
  border-block-start: 1px solid var(--line);
  border-radius: 0;
  color: var(--brand-700);
  font-size: var(--fsz-body);
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  text-align: start;
  transition: background var(--t-fast);
}
.srf-menu-reset:hover { background: var(--brand-tint); }

/* ─── Live test-stats toggle (yardlive FilterBar leadingAction) ───
   Toolbar icon-button + portaled KPI popover. The button mirrors
   `.density-icon-btn` (md icon-only tier); the popover is portaled
   to <body> so it carries its own `position: fixed` + z-index and
   is unaffected by the toolbar's stacking context. */
.lts-btn {
  /* md icon-only tier — see tokens.css `--cta-h-*`. Matches
     `.density-icon-btn` / `.srf-menu-btn`. */
  position: relative;
  width: var(--cta-h-md); height: var(--cta-h-md);
  display: inline-grid; place-items: center;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  padding: 0;
  transition: all var(--t-fast);
}
.lts-btn:hover,
.lts-btn.is-open {
  background: var(--brand-tint);
  color: var(--ink);
}
.lts-btn.is-open { color: var(--brand-700); }
.lts-btn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* Popover card — portaled to <body>; `position: fixed` is set
   inline by the component (anchored to the button). */
.lts-pop {
  position: fixed;
  min-inline-size: 260px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-popover, var(--sh-md));
  padding: 12px;
  /* Portaled out of the toolbar — sits at the popover tier. Falls
     back to the dropdown tier, then a literal that clears the
     sibling toolbar menus (z-index: 200). */
  z-index: var(--z-popover, var(--z-dropdown, 200));
  /* Clip the draining bar to the card's rounded top corners. */
  overflow: hidden;
}
.lts-pop-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-block-end: 10px;
}
.lts-pop-title {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
}
[dir="rtl"] .lts-pop-title { letter-spacing: 0; text-transform: none; }

/* Pin toggle — icon button at the header corner. Brand-tinted when
   pinned (the popover then ignores the auto-close timer). */
.lts-pin {
  flex-shrink: 0;
  display: inline-grid; place-items: center;
  width: 24px; height: 24px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  color: var(--ink-3);
  cursor: pointer;
  transition: all var(--t-fast);
}
.lts-pin:hover {
  background: var(--brand-tint);
  color: var(--ink-2);
}
.lts-pin.is-pinned {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 22%, var(--line));
  color: var(--brand-600);
}
.lts-pin:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}

/* Stats body — Scheduled hero (the master number) on the start side,
   the Completed pass/fail/absent breakdown as a color-dotted legend
   on the end side. A strip, not a grid of equal squares. */
.lts-body {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}
.lts-hero {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex-shrink: 0;
}
.lts-hero-value {
  font-size: var(--fsz-display);
  font-weight: 700;
  line-height: 1.05;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.lts-hero-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  max-inline-size: 9ch;
}
/* Completed breakdown — subsidiary rows; dots match the pass-rate
   bar segments (--pass / --fail / --absent). */
.lts-legend {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
  min-inline-size: 0;
}
.lts-legend-head {
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-2);
  margin-block-end: 2px;
}
.lts-legend-row {
  display: flex;
  align-items: center;
  gap: 7px;
}
.lts-legend-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.lts-legend-dot.is-pass   { background: var(--pass); }
.lts-legend-dot.is-fail   { background: var(--fail); }
.lts-legend-dot.is-absent { background: var(--absent); }
.lts-legend-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lts-legend-num {
  margin-inline-start: auto;
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pass-rate — the dashboard's segmented bar, reusing the global
   `.rate-cell-bar` / `.rate-cell-seg` classes for pixel-identical
   geometry + colors. Shown saturated by default: the dashboard mutes
   fail/absent until row-hover, but this popover is the focus, so the
   active colors read immediately. */
.lts-rate {
  margin-block-start: 12px;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--line);
}
.lts-rate-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-block-end: 6px;
}
.lts-rate-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}
.lts-rate-pct {
  font-size: var(--fsz-body);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.lts-rate .rate-cell-seg.is-fail   { background: var(--fail); }
.lts-rate .rate-cell-seg.is-absent { background: var(--absent); }
/* Hover-aware rate header — hovering a bar segment or a legend row
   sets data-metric, recoloring the label + % to that metric. Default
   (empty data-metric) keeps the resting Pass-rate styling. */
.lts-rate-head[data-metric="pass"]   .lts-rate-label,
.lts-rate-head[data-metric="pass"]   .lts-rate-pct   { color: var(--pass); }
.lts-rate-head[data-metric="fail"]   .lts-rate-label,
.lts-rate-head[data-metric="fail"]   .lts-rate-pct   { color: var(--fail); }
.lts-rate-head[data-metric="absent"] .lts-rate-label,
.lts-rate-head[data-metric="absent"] .lts-rate-pct   { color: var(--absent); }
/* Legend rows double as hover targets for the rate swap. */
.lts-legend-row { border-radius: 6px; padding-inline: 4px; margin-inline: -4px; }
.lts-legend-row:hover { background: color-mix(in oklab, var(--ink) 5%, transparent); }

/* Draining countdown bar — BOTTOM edge, unified with the canvas view-all
   completion toast (.tc-va-toast-bar) so the auto-close cue sits in the
   same place everywhere. Depletes left→right over the auto-close window;
   `animationDuration` + play-state are set inline (paused on hover,
   remounted to restart on resume). The transform-origin flips for RTL so
   the bar drains toward the reading-start edge. Hidden when pinned. */
.lts-countdown {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 2px;
  background: var(--brand-500);
  transform-origin: left center;
  animation-name: lts-drain;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  will-change: transform;
}
[dir="rtl"] .lts-countdown { transform-origin: right center; }
@keyframes lts-drain { from { transform: scaleX(1); } to { transform: scaleX(0); } }
@media (prefers-reduced-motion: reduce) {
  /* Belt-and-suspenders: the component already skips rendering the
     bar under reduced-motion, but disable the animation here too in
     case it's ever rendered. */
  .lts-countdown { animation: none; transform: scaleX(0); }
}

/* ─── Direction-aware icons (RTL mirror) ─────────────────────────
   Some icons represent a direction of travel and should mirror to
   face the reading direction — same convention as a road sign.
   The `Icon` component in components.jsx now emits a `data-icon`
   attribute on every SVG so we can target by name without
   touching every callsite. Add new entries here as we identify
   more direction-of-travel icons.

   Currently mirrored:
     • car — vehicle icon; faces right in LTR, left in RTL.

   NOT mirrored (intentionally — these have universal LTR
   conventions even in RTL contexts):
     • play — transport control, always points "into" the action.
     • externalLink — universal "opens elsewhere" symbol.
     • chevRight / chevLeft — code-side paired (we swap the name
       per language), no CSS mirror needed. */
[dir="rtl"] svg[data-icon="car"] { transform: scaleX(-1); }
/* logIn — door+arrow glyph pointing right (into the room). In RTL
   the arrow should point left (into the room from the reader's
   reading direction). Same scaleX(-1) trick as the car icon. */
[dir="rtl"] svg[data-icon="logIn"] { transform: scaleX(-1); }
/* undo — curved arrow pointing left (the canonical "go back / return"
   glyph in LTR). In RTL the reading direction reverses, so the arrow
   should point right to read as a return-to-the-previous-position
   affordance. Used only on yardlive's Return-to-waitlist buttons
   (`.ylc-assign-return` + `.ylc-strip-return-btn`); flipping the
   icon globally via `data-icon` keeps both surfaces in sync without
   per-button overrides. */
[dir="rtl"] svg[data-icon="undo"] { transform: scaleX(-1); }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  padding: 7px 12px;
  font-size: var(--fsz-body);
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: all var(--t-fast);
  white-space: nowrap;
  /* Reset anchor-default text-decoration. Without this, any
     `<a class="btn …">` (e.g. the test-canvas sidebar's
     "View Test Certificate" anchor styled as a ghost button)
     renders with the browser-default underline, which visually
     swamps every other style property and makes the button look
     like a plain underlined link regardless of the .btn-*
     variant applied. */
  text-decoration: none;
  /* CTAs keep their intrinsic content width regardless of where
     they're dropped. Without this, any parent flex container with
     `flex: 1 1 0` on its children (e.g., narrow-viewport action
     rows) stretches the button to fill — which reads as the button
     "owning" the row instead of being a discrete affordance. If a
     specific layout really needs a full-width button (typically
     bottom-of-form submits in modals), use the explicit
     `.btn.is-block` modifier below. */
  flex: 0 0 auto;
}
/* Anchor-as-button color cleanup — without these, `a.btn:visited`
   would inherit the browser-default purple visited-link color in
   most browsers, leaking through any .btn-* variant's color rule. */
.btn:link, .btn:visited { color: inherit; }
.btn:hover { text-decoration: none; }
.btn.is-block { flex: 1 1 auto; inline-size: 100%; justify-content: center; }
.btn-ghost { background: transparent; color: var(--ink-2); }
.btn-ghost:hover { background: var(--brand-tint); color: var(--ink); }
.btn-primary {
  background: var(--brand-cta);
  color: var(--brand-ink);
  box-shadow: var(--sh-xs);
}
.btn-primary:hover { background: color-mix(in oklab, var(--brand-cta), #000 8%); }
.btn-sm { padding: 5px 9px; font-size: var(--fsz-label); border-radius: var(--r-sm); }

/* ─────────── INLINE LINK / TEXT CTA STANDARD ───────────────
   Unified treatment for any clickable inline text — "View all",
   "See details", "Forgot password", inline names linking to
   profiles, dashboard "see more" links, etc. ONE consistent
   look across the entire app:
     · Default: brand-700 text, weight 600, NO underline.
     · Hover  : color darkens to brand-800 (with brand-700 fallback)
                + soft brand-tint background. Still NO underline.
     · Focus  : standard 2px ring (matches the .cell-link / .btn
                focus convention).
   The padding/margin trick (2px 6px padding + matching negative
   margin) gives the hover background tint breathing room without
   changing the link's layout footprint — so inline text doesn't
   shift on hover.
   New code should use `.link`. Existing context-specific classes
   (`.cell-link`, `.btn-text`, `.lt-h1-cta`, `.d2-digest-link`,
   `.d2-section-link`, `.hp-sub-link`, `.auth-link`, `.live-test-cta`,
   `.lt-v1-cta a`) all follow the same hover/focus pattern. */
.link {
  color: var(--brand-700);
  text-decoration: none;
  font-weight: 600;
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.link:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.link:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* ─────────── LINK-CTA — Brand-tinted touch-target CTA ───────────
   Same visual language as `.link` (brand-700 text, no underline,
   brand-tint hover) but with a 32px touch-target floor + slightly
   bigger hit padding. Use for standalone CTAs in cards / footers
   (e.g. "View details", "See all", "View Test Certificate") where
   the negative-margin trick of `.link` would crowd the layout.
   This is the canonical replacement for the per-module ad-hoc
   classes (.lt-h1-cta / .tc-sidebar-cta) that used to redefine
   the same pattern in their own files. Use this everywhere now. */
.link-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  min-block-size: var(--cta-h-md);
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--brand-700);
  text-decoration: none;
  background: transparent;
  border-radius: var(--r-sm);
  transition: color var(--t-fast), background var(--t-fast);
}
.link-cta:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.link-cta:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* ─────────── DATA TABLE ─────────── */
.table-card {
  background: var(--bg-raised);
  /* 1.5px (vs hairline 1px) paired with the slightly mixed
     `--tbl-frame-border` token — system-wide table frame standard
     finalized on the yardlive page; see token comment in tokens.css. */
  border: 1.5px solid var(--tbl-frame-border);
  border-radius: var(--tbl-frame-radius);
  /* Clip horizontally so rounded corners crop side overflow.
     `overflow-y: visible` is intended to let per-cell tooltips rise
     above row 1, but per CSS spec, mixing clip + visible computes
     both axes to clip — so without `overflow-clip-margin` the row-1
     edit/activate/duplicate/schedule tooltips were being clipped at
     the card's top edge. Margin of 32px gives descendants room to
     paint that far outside the clip box. */
  overflow-x: clip;
  overflow-y: visible;
  overflow-clip-margin: 32px;
  box-shadow: var(--sh-xs);
}

.table-scroll {
  overflow-x: clip;
  overflow-y: visible;
  /* Same trick as `.rpt-table-wrap`: per CSS spec, mixing clip + visible
     computes both axes to clip, which means tooltips on action buttons
     in the FIRST row of the table get clipped at the wrapper's top edge.
     `overflow-clip-margin` lets descendants paint up to 32px outside
     the clip box — enough headroom for the row-1 edit / activate /
     duplicate / schedule tooltips to show in full. */
  overflow-clip-margin: 32px;
}

/* Saved Tests — dynamic table width + scroll handoff.
   The table's pixel width is set inline by React = sum of the
   currently-visible columns' widths. Each column stays at its
   tuned pixel size; hiding a column shrinks the table by exactly
   that column's width, instead of letting the remaining columns
   stretch to fill the wrapper.
   The .is-overflowing class on .table-scroll is toggled by a
   ResizeObserver in DataTable when the table is wider than its
   wrapper. That's when we switch to overflow-x: auto and drop the
   thead's sticky position (the CSS spec coerces overflow-y to
   auto when overflow-x becomes auto, which would hijack sticky's
   scroll context). When the table fits, the wrap stays clip and
   the thead sticks normally. */
.table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing {
  overflow-x: auto;
  /* Subtle right-edge fade signaling "more content this way →".
     Pure CSS — no extra DOM. The fade is anchored to the SCROLL
     container's viewport (not the scrolling content) via a sticky
     pseudo, so it stays pinned at the visible right edge as the
     user scrolls. Hidden once the user has scrolled all the way
     to the end via the `.is-scrolled-end` class toggled in JS. */
  position: relative;
}
.table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing::after {
  content: '';
  position: sticky;
  inset-block: 0;
  inset-inline-end: 0;
  display: block;
  block-size: 100%;
  inline-size: 36px;
  margin-inline-start: -36px;
  background: linear-gradient(to right, transparent, var(--bg-raised) 85%);
  pointer-events: none;
  flex-shrink: 0;
  z-index: 2;
  transition: opacity 160ms ease;
}
[dir="rtl"] .table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing::after {
  background: linear-gradient(to left, transparent, var(--bg-raised) 85%);
}
/* Hide the right-edge fade when scrolled all the way to the end —
   `.is-scrolled-end` is toggled by the same useEffect that toggles
   `.is-overflowing` (component already tracks scroll position). */
.table-card:has(.data-table--saved-tests) .table-scroll.is-scrolled-end::after {
  opacity: 0;
}
/* Force a visible horizontal scrollbar on overflowing tables at
   touch/narrow viewports so the affordance is obvious (default
   macOS auto-hide scrollbars make scroll discovery harder). */
@media (max-width: 720px) {
  .table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing {
    scrollbar-width: thin;
    scrollbar-color: color-mix(in oklab, var(--brand-500) 35%, transparent) transparent;
  }
  .table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing::-webkit-scrollbar {
    block-size: 6px;
  }
  .table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing::-webkit-scrollbar-thumb {
    background: color-mix(in oklab, var(--brand-500) 35%, transparent);
    border-radius: 3px;
  }
}
.table-card:has(.data-table--saved-tests) .table-scroll.is-overflowing
  .data-table--saved-tests thead th { position: static; }

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  /* Body-baseline (14px) per the design-system rule. Was --fsz-label
     (12px) — that was an artifact of the Phase 3 mass migration which
     preserved the original 12px size rather than upgrading to the
     14px body baseline. Made `.pr-num` (14px) visibly larger than
     surrounding numeric cells. */
  font-size: var(--fsz-body);
  font-family: var(--font-ui);
  table-layout: fixed;
}
/* Row-arrival pulse — fires once on a `<tr>` that was just pushed
   into a table from another surface (e.g. a yard-live row that
   completed and is now landing in the Completed table). The bg
   fades from a result-tinted color (green / red / red-DQ) back to
   transparent over 1.2s, drawing the eye to the new entry without
   keeping a persistent highlight that would clutter the table on
   subsequent scrolls. Tone is set by the `is-arrived-*` modifier.
   Respects `prefers-reduced-motion`. */
.data-table tbody tr.row-just-arrived > td {
  /* 5s prolonged flashing pulse — 5 iterations of a 1s ease-out
     fade. Each iteration restarts at the result-tinted color and
     fades to transparent. Net effect: the new row visibly
     "blinks" 5 times over 5 seconds so an operator who glances
     over a beat after the test completed still catches the new
     arrival. Was a single 2s fade; users reported it was easy to
     miss if they were focused on the canvas. */
  animation: row-arrival-pulse 1s ease-out 5 both;
}
.data-table tbody tr.row-just-arrived.is-arrived-pass > td {
  --row-arrival-tone: color-mix(in oklab, var(--ok) 18%, var(--bg-raised));
}
.data-table tbody tr.row-just-arrived.is-arrived-fail > td,
.data-table tbody tr.row-just-arrived.is-arrived-dq > td {
  --row-arrival-tone: color-mix(in oklab, var(--err) 16%, var(--bg-raised));
}
@keyframes row-arrival-pulse {
  0%   { background: var(--row-arrival-tone, var(--bg-raised)); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .data-table tbody tr.row-just-arrived > td { animation: none; }
}
.data-table td,
.data-table .mono,
.data-table .cell-id,
.data-table .cell-muted,
.data-table .student-name .cell-link,
.data-table .examiner-cell .cell-link,
.data-table .vehicle-tag,
.data-table .vehicle-tag .mono {
  font-family: var(--font-ui);
  font-size: var(--fsz-body);
}
/* `.ident` cells re-assert the monospace IDENTIFIER treatment, overriding the
   UI-font default above (same specificity, later source order wins). Identifier
   columns — cell-id traffic files, vehicle codes — get JetBrains Mono at 0.92em.
   The `.cell-id` 600 weight (below) still applies; this only sets family+size. */
.data-table .ident { font-family: var(--font-mono); font-size: 0.92em; }
/* Arabic UI needs slightly larger text to feel balanced against Latin sizing. */
[dir="rtl"] .th-btn { font-size: var(--fsz-caption); letter-spacing: 0.02em; }
[dir="rtl"] .table-count {
  font-size: var(--fsz-label);
  letter-spacing: 0;
  font-family: var(--font-ui);
  color: var(--ink-3);
}
[dir="rtl"] .page-size { font-size: var(--fsz-label); }
[dir="rtl"] .page-btn { font-size: var(--fsz-body); }
[dir="rtl"] .filter-menu-item { font-size: var(--fsz-body); }
[dir="rtl"] .filter-pill { font-size: var(--fsz-label); }
[dir="rtl"] .page-size select { font-size: var(--fsz-body); }
.data-table .mono { font-variant-numeric: tabular-nums; letter-spacing: 0; }
.data-table td { font-weight: 400; }
/* Identity column (the first traffic-file cell) follows the project's
   "only the first column is bold" rule — 600 weight. Student-name is
   not the first column, but it's the human-readable identity humans
   recognize first, so it gets a moderate 500 weight to lift it above
   plain data without competing with the column-1 bold. */
.data-table .cell-id { font-weight: 600; }
.data-table .student-name .cell-link { font-weight: 500; }

.data-table thead th {
  /* Stale comment was "TopNav (64) + compact strip (40) = 104". The
     TopNav is gone since the SideNav rollout — the SideNav is on
     the leading edge, not the top edge. The only thing sticking
     above the table thead is the compact-strip (40px) when
     scrolled past the page-header. So `top: 40px` is the new
     correct value at desktop. On mobile the mobile-top-strip
     (48px) sits above the compact-strip — handled below. */
  position: sticky; top: 40px;
  background: var(--tbl-header-bg);
  border-bottom: 1px solid var(--tbl-divider);
  text-align: start;
  padding: 0;
  font-weight: 500;
  z-index: 4;
  height: 40px;
  transition: box-shadow 120ms ease;
}
.density-cozy .data-table thead th,
.density-compact .data-table thead th { height: 40px; }
.data-table thead.is-stuck th {
  box-shadow: 0 4px 8px -6px color-mix(in oklab, var(--ink) 28%, transparent);
  height: 34px;
}
.data-table thead.is-stuck .th-btn {
  padding: 4px 8px;
  font-size: var(--fsz-caption);
}
.th-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 10px 6px;
  background: transparent;
  border: 0;
  font-size: var(--fsz-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--ink-3);
  cursor: pointer;
  font-family: inherit;
  width: 100%;
  white-space: nowrap;
}
.th-btn:hover { color: var(--ink); }
/* Direct-child SVGs only (legacy headers without an icon slot). The new
   .th-icon slot manages its own SVG opacities — see the .th-icon rules
   below. */
.th-btn > svg { opacity: 0.6; }
.th-btn.is-sorted { color: var(--ink); }
.th-btn.is-sorted > svg { opacity: 1; color: var(--brand-600); }
/* When the active-sort arrow is showing, give it the brand color (the slot
   logic already handles its visibility). */
.th-btn.is-sorted .th-icon-active { color: var(--brand-600); }

[style*="text-align: center"] .th-btn { justify-content: center; }

.data-table tbody {
  --row-line: color-mix(in srgb, var(--ink) 10%, transparent);
}
.data-table tbody tr {
  transition: background var(--t-fast);
}
/* Use box-shadow on the cell for reliable row lines under border-collapse: separate */
.data-table tbody td { box-shadow: inset 0 -1px 0 0 var(--row-line); }
.data-table tbody tr:first-child td { box-shadow: inset 0 1px 0 0 var(--row-line), inset 0 -1px 0 0 var(--row-line); }
/* Outer corner rounding via the corner cells themselves — works
   alongside .table-card's overflow-x:clip + overflow-y:visible
   without needing full overflow:hidden (which would break the cell
   tooltips that intentionally rise above row 1). */
.data-table thead th:first-child { border-start-start-radius: var(--tbl-frame-radius); }
.data-table thead th:last-child  { border-start-end-radius: var(--tbl-frame-radius); }
/* When the header is STUCK (floating mid-scroll via position:sticky),
   it has detached from the card's rounded top corners — the card top
   (and the --bg-raised that fills behind these arcs at rest) has
   scrolled away. Leaving the radius on would expose the page
   background through the corner arcs as white triangles. Square the
   top corners while stuck so the floating header is a clean edge-to-
   edge rectangle; the corners round back the moment it re-docks at
   the card top. Logical properties → RTL-safe. */
.data-table thead.is-stuck th:first-child { border-start-start-radius: 0; }
.data-table thead.is-stuck th:last-child  { border-start-end-radius: 0; }

/* Frame-perfect companion to the .is-stuck squaring above. The JS observer
   re-checks on rAF after scroll events, so a fast scroll can leave a 1-frame
   window where a freshly-pinned header still shows its rounded arcs (white
   bleed). Scroll-driven animations square the top corners straight from
   scroll position — no rAF lag, no re-render coupling — so the bleed can't
   appear. The card's own top corners square in lockstep so a squared header
   never gaps against a still-rounded card. @supports-gated; engines without
   scroll timelines fall back to the .is-stuck observer. scroll(nearest)
   adapts to whichever ancestor scrolls (page on Saved Tests, .d2-main on
   dashboards). RTL-safe via logical radii. */
@supports (animation-timeline: scroll()) {
  @keyframes ive-square-top-corners {
    to { border-start-start-radius: 0; border-start-end-radius: 0; }
  }
  :is(.data-table, .branch-cmp-tbl) thead th:first-child,
  :is(.data-table, .branch-cmp-tbl) thead th:last-child,
  .table-card:has(.data-table, .branch-cmp-tbl) {
    animation: ive-square-top-corners steps(1, jump-end) both;
    animation-timeline: scroll(nearest block);
    animation-range: 0px 2px;
  }
}
.data-table tbody tr:last-child td:first-child { border-end-start-radius: var(--tbl-frame-radius); }
.data-table tbody tr:last-child td:last-child  { border-end-end-radius: var(--tbl-frame-radius); }
.data-table tbody tr:nth-child(even) { background: var(--tbl-row-stripe-bg); }
.data-table tbody tr:hover,
.data-table tbody tr:nth-child(even):hover {
  background: var(--tbl-row-hover-bg);
}

.data-table td {
  padding: 6px 6px;
  vertical-align: middle;
  color: var(--ink);
  white-space: nowrap;
  box-sizing: border-box;
  overflow: hidden;
}
.data-table td:first-child { padding-inline-start: 14px; }
.data-table td:last-child { padding-inline-end: 14px; }
.data-table thead th:first-child .th-btn { padding-inline-start: 14px; }
.data-table thead th:last-child .th-btn { padding-inline-end: 14px; }
.density-compact .data-table tbody td,
.density-compact .data-table tbody tr { height: 36px; }
.density-cozy .data-table tbody td,
.density-cozy .data-table tbody tr { height: 60px; }
/* Tighter cell + header padding in compact density. The default 6px
   vertical / 14px horizontal padding wastes row height on a 36px row
   that no longer carries an avatar. Row height bumped 32 → 36 so
   the body-size cell text (`--fsz-body` 14px) has enough vertical
   clearance under its natural line-height (~20px). 36px still
   saves ~40% per row vs cozy (60px). */
.density-compact .data-table tbody td { padding: 4px 8px; }
.density-compact .data-table tbody td:first-child { padding-inline-start: 12px; }
.density-compact .data-table tbody td:last-child  { padding-inline-end: 12px; }
/* Header stays at the same dimensions in both density modes — only
   body rows tighten when switching cozy ↔ compact. The `thead th` /
   `.th-btn` defaults higher in this file (height 40px, padding 10/6,
   font-size 10.5) carry through unchanged. */
/* Avatar isn't rendered in compact mode (per DataTable JSX), but
   collapse the gap on the cell wrappers anyway so a future avatar
   reintroduction doesn't accidentally widen the row. */
.density-compact .student-cell,
.density-compact .examiner-cell { gap: 0; }
/* Cell font sizes are intentionally NOT reduced in compact density —
   density compresses padding + row height, not text. Earlier this
   block dropped 6 cell-types from `--fsz-body` (14px) to `--fsz-label`
   (12px), which was below comfortable reading for tabular data and
   created a readability gap between cozy and compact. Now both modes
   render at body size; compact still gains screen real estate via
   the tighter row height + padding above. */

.cell-id {
  font-size: var(--fsz-label);
  color: var(--ink);
  font-weight: 600;
}
/* `.data-table td` defines `color: var(--ink)` at (0,0,1,1) which
   beats a plain `.cell-muted` (0,0,1,0). Bump specificity so muted
   row cells (e.g. dates in the L3 test list) actually render in
   ink-3 instead of inheriting the default ink color. */
.cell-muted,
.data-table .cell-muted { color: var(--ink-3); }
.cell-empty,
.data-table .cell-empty { color: var(--ink-4); }

/* Student cell */
.student-cell {
  display: flex; align-items: center; gap: 8px;
  min-width: 0;
}
.student-ava {
  width: 26px; height: 26px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
}
.density-cozy .student-ava { width: 34px; height: 34px; }
.student-name { min-width: 0; }
.student-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* Cell link — a button that looks like the previous <a>. Used for student
   and examiner names so they stay clickable but don't act as phantom links. */
.cell-link {
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  text-align: start;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.cell-link:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 2px; }
.student-cell .student-ava,
.examiner-cell .examiner-ava { cursor: pointer; transition: box-shadow var(--t-fast); }
/* Avatar hover ring — solid brand-500 at 2px to match the test-
   canvas avatar (`.tc-identity-avatar:hover`), the canonical
   visibility target for "person photo is clickable" across the
   system. Earlier `var(--brand-tint)` recipe was too faint to read
   on the table background. */
.student-cell .student-ava:hover,
.examiner-cell .examiner-ava:hover { box-shadow: 0 0 0 2px var(--brand-500); }
.student-name .cell-link {
  color: var(--ink);
  text-decoration: none;
  font-size: var(--fsz-label);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  /* No text-overflow:ellipsis here — the new marquee uses a fade-gradient
     mask instead so the inner span can transform-slide past the edge. */
  display: block;
  max-width: 150px;
}
.student-name .cell-link { max-width: 240px; }
.density-compact .student-name .cell-link { max-width: 220px; }

/* Slide-on-hover for truncated names is driven by JS (it measures
   the exact overflow and sets text-indent inline) so the animation
   stops at the last letter and only runs when the name doesn't fit. */
.student-name .cell-link,
.examiner-cell .cell-link,
.student-name-sub {
  text-indent: 0;
}
/* Cursor override — system rule says ONLY the avatar opens the
   profile popover. The name reuses the `.cell-link` styling family
   for typography/marquee but is rendered as a static `<span>` in
   the DataTable (no onClick). Override the `cell-link` default
   `cursor: pointer` (inherited from its button-reset origins) so
   the name doesn't falsely advertise itself as clickable. */
.student-name .cell-link,
.examiner-cell .cell-link { cursor: default; }
/* (Name color hover removed — system rule: name is informational
   text, no hover state, no color shift. Avatar ring is the sole
   "this is a clickable person" affordance.) */
.student-name-sub {
  display: block;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-weight: 400;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  /* No text-overflow:ellipsis — the new marquee uses a fade-gradient mask. */
  max-width: 150px;
  font-family: var(--font-ar);
}
.student-name-sub { max-width: 240px; }
[dir="ltr"] .student-name-sub { text-align: left; }

/* Examiner cell */
.examiner-cell { display: flex; align-items: center; gap: 8px; }
.examiner-ava {
  width: 26px; height: 26px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
}
.density-cozy .examiner-ava { width: 34px; height: 34px; }
.examiner-cell .cell-link {
  color: var(--ink);
  text-decoration: none;
  font-size: var(--fsz-label);
  white-space: nowrap;
  overflow: hidden;
  /* No text-overflow:ellipsis — the marquee uses a fade-gradient mask. */
  max-width: 210px;
}
/* Name stack inside the examiner cell — same layout as .student-name so the
   secondary (Arabic / English) line renders directly under the headline name
   in cozy density. */
.examiner-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.examiner-name-sub {
  display: block;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-weight: 400;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  max-width: 210px;
  font-family: var(--font-ar);
}
[dir="ltr"] .examiner-name-sub { text-align: left; }
.examiner-cell { gap: 5px; }
/* (Examiner name hover color removed — system rule: name stays
   informational; avatar is the only clickable affordance.) */

/* Vehicle tag — minimal: a colored category dot + plate text. The dot's
   hue communicates Manual vs Automatic at a glance; the column header's
   car icon already says "this column = vehicle", so no per-row icon. */
.vehicle-tag {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--fsz-label);
  color: var(--ink);
  white-space: nowrap;
}
/* The data-table cells use overflow:hidden so other columns can ellipsize.
   The vehicle cell needs to escape that so its tooltip isn't clipped.
   Positioning + z-index for the tooltip itself live in the unified
   `.data-table tbody [data-tip]::after` rule below, so every in-table
   tooltip behaves identically. */
.data-table td.cell-vehicle { overflow: visible; }
.vehicle-tag::before {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 12%, transparent);
}
/* Manual — warm amber */
.vehicle-tag.is-manual::before { background: oklch(0.66 0.16 55); color: oklch(0.66 0.16 55); }
/* Automatic — calm cyan */
.vehicle-tag.is-auto::before   { background: oklch(0.60 0.13 215); color: oklch(0.60 0.13 215); }
[data-theme="dark"] .vehicle-tag.is-manual::before { background: oklch(0.74 0.16 55); color: oklch(0.74 0.16 55); }
[data-theme="dark"] .vehicle-tag.is-auto::before   { background: oklch(0.72 0.13 215); color: oklch(0.72 0.13 215); }

/* Result pills — fixed semantic colors (not impacted by brand).
   Single source of truth for pass / fail / absent / pending used by:
   - savedtests result column
   - reports tables
   - dashboard institute cards (rate bar + strip + popover)
   - dashboard KPI strip segment dots
   Absent uses neutral surface tokens so it visually matches the
   vehicle chip and reads as "no result" rather than a warning.
   Pending is a calm blue — clearly distinct from the red of fail. */
:root {
  --pass: oklch(0.56 0.13 155);
  --pass-bg: oklch(0.96 0.04 155);
  --pass-border: oklch(0.85 0.08 155);
  --fail: oklch(0.55 0.17 25);
  --fail-bg: oklch(0.96 0.04 25);
  --fail-border: oklch(0.85 0.10 25);
  --absent: var(--ink-2);
  --absent-bg: var(--bg-sunken);
  --absent-border: var(--line);
  /* --pending: was hardcoded #2563eb (~oklch(0.55 0.17 264)) which broke
     dark-mode parity. Converted to oklch on the same blue hue (264) so
     the dark-theme override below can shift lightness/chroma cleanly. */
  --pending: oklch(0.55 0.17 264);
  --pending-bg: oklch(0.96 0.04 264);
  --pending-border: oklch(0.85 0.10 264);
}
[data-theme="dark"] {
  --pass: oklch(0.78 0.14 155);
  --pass-bg: oklch(0.28 0.06 155);
  --pass-border: oklch(0.42 0.10 155);
  --fail: oklch(0.74 0.16 25);
  --fail-bg: oklch(0.30 0.07 25);
  --fail-border: oklch(0.45 0.12 25);
  --absent: var(--ink-2);
  --absent-bg: var(--bg-sunken);
  --absent-border: var(--line);
  /* Dark-theme override — same hue (264) as the light value so the
     two states share one logical blue, lifted to L 0.72 for
     readability on dark surfaces. (Was hardcoded #60a5fa — caught
     in the post-Phase-5 validation audit.) */
  --pending: oklch(0.72 0.17 264);
  --pending-bg: oklch(0.30 0.07 264);
  --pending-border: oklch(0.45 0.12 264);
}

/* ── Pill primitive ──────────────────────────────────────────────
   Master pill component unifying status / result / live-indicator
   pills under one anatomy. Tone variants compose fill + text +
   border from a single semantic token. Modifiers add a dot, pulse
   animation, fixed width (for Pass/Fail/Absent alignment), or
   compact density.

   Migration map (Phase 2 — not yet shipped):
     .result-pill.pass         → .pill.is-ok.is-fixed-w
     .result-pill.fail         → .pill.is-err.is-fixed-w
     .result-pill.absent       → .pill.is-neutral.is-fixed-w
     .ylt-status-pill.pending  → .pill.is-neutral + <span class="pill-dot">
     .ylt-status-pill.checked  → .pill.is-info    + <span class="pill-dot">
     .ylt-status-pill.assigned → .pill.is-ok      + <span class="pill-dot">
     .lt-v1-current            → .pill.is-ok      + <span class="pill-dot has-pulse"></span>
     .lt-v1-brake-pill         → .pill.is-err.has-alarm
                                  + <span class="pill-icon"><svg>⚠</svg></span>
     .dash-live-badge (L2/L3)  → .pill.is-neutral.is-uppercase
                                  + <span class="pill-dot is-err has-pulse">LIVE</span>
     .dash-live-badge (L1)     → .pill.is-paper.is-uppercase
                                  + <span class="pill-dot is-err has-pulse">LIVE</span>
*/
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.4;
  white-space: nowrap;
  border: 1px solid transparent;
  box-sizing: border-box;
}
.pill-dot {
  inline-size: 6px; block-size: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* Tone variants — use the *-soft tone family for bg (chroma ~0.04-0.06)
   rather than *-tint (chroma ~0.018) so the pill reads as visibly
   tinted, matching the existing .result-pill aesthetic. */
.pill.is-ok      { background: var(--ok-soft);    color: var(--ok);        border-color: color-mix(in oklab, var(--ok)        26%, var(--line)); }
.pill.is-err     { background: var(--err-soft);   color: var(--err);       border-color: color-mix(in oklab, var(--err)       26%, var(--line)); }
.pill.is-warn    { background: var(--warn-soft);  color: var(--warn);      border-color: color-mix(in oklab, var(--warn)      26%, var(--line)); }
.pill.is-info    { background: var(--info-soft);  color: var(--info);      border-color: color-mix(in oklab, var(--info)      26%, var(--line)); }
.pill.is-brand   { background: var(--brand-tint); color: var(--brand-700); border-color: color-mix(in oklab, var(--brand-500) 26%, var(--line)); }
.pill.is-neutral { background: var(--bg-sunken);  color: var(--ink-3);     border-color: var(--line); }
/* Neutral in dark mode: the default tokens (--bg-sunken / --bg-raised)
   are too close to --bg in lightness (0.03 step), so the pill nearly
   disappears against the page. Use a hardcoded elevated gray
   (lightness 0.28 — ~8% above page bg) with a stronger border so the
   chip clearly reads as a soft elevated tile, matching the light-mode
   contrast ratio (pill ~7% darker than page). */
[data-theme="dark"] .pill.is-neutral {
  background: oklch(0.28 0.008 var(--brand-h));
  color: var(--ink-2);
  border-color: oklch(0.36 0.012 var(--brand-h));
}

/* Paper variant — sibling to `.is-neutral` for the inverse-contrast
   case: when the pill sits on a sunken surface (e.g., L1 dashboard
   page over the sunken page bg), it needs to be ELEVATED (white card)
   rather than recessed. Same neutral semantic, different chip
   direction. Used by the L1 LIVE badge migration. */
.pill.is-paper { background: var(--bg-raised); color: var(--ink-2); border-color: var(--line); }
[data-theme="dark"] .pill.is-paper {
  background: oklch(0.32 0.010 var(--brand-h));
  color: var(--ink-2);
  border-color: oklch(0.40 0.014 var(--brand-h));
}

/* Per-dot tone overrides — let the dot color differ from the pill
   color. Used by LIVE-style chips: neutral pill body + err-colored
   pulsing dot (the dot carries the state semantic, the pill body
   describes the entity). */
/* Per-dot tone overrides — set BOTH `background` (the dot fill) AND
   `color` (so `currentColor` in the pulse box-shadow resolves to the
   tone, not to the pill body's text color). Without the `color` mirror
   the pulse ring goes gray on neutral-bodied pills like LIVE. */
.pill-dot.is-ok    { background: var(--ok);        color: var(--ok); }
.pill-dot.is-err   { background: var(--err);       color: var(--err); }
.pill-dot.is-warn  { background: var(--warn);      color: var(--warn); }
.pill-dot.is-info  { background: var(--info);      color: var(--info); }
.pill-dot.is-brand { background: var(--brand-600); color: var(--brand-600); }

/* Uppercase modifier — for LIVE-style typography. Uppercase + 0.06em
   tracking matches the existing .dash-live-badge treatment so the
   migration target is a 1:1 swap. */
.pill.is-uppercase {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 700;
}

/* Modifiers */
.pill.is-fixed-w {
  display: inline-grid; place-items: center;
  width: 64px; height: 22px; padding: 0 10px;
}
[dir="rtl"] .pill.is-fixed-w { width: 56px; }

/* `.is-compact` — mid density for dense data rows. Scales the entire
   anatomy proportionally (font + dot + icon + padding) rather than
   just padding. Reserved for grid/table compact view. */
.pill.is-compact { padding: 2px 8px; gap: 4px; font-size: var(--fsz-caption); }
.pill.is-compact > .pill-dot { inline-size: 5px; block-size: 5px; }
.pill.is-compact > .pill-icon { inline-size: 12px; block-size: 12px; }

/* `.is-xs` — smallest tier for chart contexts (KPI deltas, axis chips,
   legend pills, in-chart annotations). Uses the chart-label font tier
   (10px) so the pill aligns with surrounding chart typography. */
.pill.is-xs { padding: 1px 6px; gap: 3px; font-size: var(--fsz-chart-label); font-weight: 700; }
.pill.is-xs > .pill-dot { inline-size: 4px; block-size: 4px; }
.pill.is-xs > .pill-icon { inline-size: 8px; block-size: 8px; }
.pill.is-xs.is-fixed-w { width: 52px; height: 18px; padding: 0 8px; }
[dir="rtl"] .pill.is-xs.is-fixed-w { width: 46px; }

.pill-dot.has-pulse {
  box-shadow: 0 0 0 0 currentColor;
  animation: pill-dot-pulse 1.6s ease-out infinite;
}
@keyframes pill-dot-pulse {
  0%   { box-shadow: 0 0 0 0 currentColor; }
  70%  { box-shadow: 0 0 0 5px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Icon affordance — sibling primitive to `.pill-dot`. Use when the
   semantic is illustrative (⚠ brake, ✓ assigned, ⏱ pending) rather
   than just "live indicator". Inherits currentColor by default; the
   tone overrides below let the icon color differ from the pill body
   (parallel to the .pill-dot.is-* pattern). */
.pill-icon {
  inline-size: 14px;
  block-size: 14px;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: currentColor;
}
.pill-icon > svg { inline-size: 100%; block-size: 100%; }
.pill-icon.is-ok    { color: var(--ok); }
.pill-icon.is-err   { color: var(--err); }
.pill-icon.is-warn  { color: var(--warn); }
.pill-icon.is-info  { color: var(--info); }
.pill-icon.is-brand { color: var(--brand-600); }

/* Body alarm — entire pill pulses, reserved for safety-critical states.
   Scoped to `.is-err` and `.is-warn` tones only: the animation is
   aggressive motion and should never apply to passive states like
   "pending" or "online". For calm live indicators use the dot-level
   `has-pulse` instead. Brake-engaged is the canonical use case. */
.pill.is-err.has-alarm,
.pill.is-warn.has-alarm {
  animation: pill-body-alarm 1.2s ease-out infinite;
}
@keyframes pill-body-alarm {
  0%   { box-shadow: 0 0 0 0 currentColor; }
  70%  { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .pill-dot.has-pulse,
  .pill.is-err.has-alarm,
  .pill.is-warn.has-alarm { animation: none; }
}

.result-cell {
  display: inline-flex; flex-direction: column; align-items: center; gap: 5px;
}
/* `.result-pill` styling MIGRATED to `.pill.is-fixed-w` + tone variants.
   The class names (`result-pill`, `.pass`, `.fail`, `.absent`) are kept
   on the markup as addressing hooks for any callsite-specific CSS, but
   carry no styling here — the `.pill` primitive owns all visual rules. */
.density-compact .result-cell { gap: 3px; }
/* When the pill carries an override icon, drop the `is-fixed-w`
   grid layout (which stacks two children into separate rows because
   it's an `inline-grid` with `place-items: center`) and revert to
   the primitive's `inline-flex` row layout so icon + label sit
   side-by-side. Width opens up just enough to fit both, with a min
   that preserves the visual rhythm of the result column. */
.pill.result-pill.has-override {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  inline-size: auto;
  min-inline-size: 64px;
  padding: 3px 10px;
}

/* Override state — icon lives INSIDE the result pill in the dot
   slot. Composes the `.pill-icon` primitive (14×14 standard size,
   currentColor) so it visually matches every other affordance icon
   in the system (e.g. assigned / brake / pending). The pill tone
   still reflects the CURRENT result; the icon overlays override-
   state context on top of the tone:
     - pending  → clock, ripple-halo pulse identical to the live
                  dot (`pill-dot-pulse` keyframe)
     - applied  → checkCircle, static (action settled, no attention)
   `.is-override-pending` adds a circular halo via box-shadow so
   the ripple reads as a clean ring around the icon, even though
   the .pill-icon container itself is square. */
.result-override-icon.is-override-pending {
  border-radius: 50%;
  box-shadow: 0 0 0 0 currentColor;
  animation: pill-dot-pulse 1.6s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .result-override-icon.is-override-pending { animation: none; }
}
/* Pending icon is rendered as a <button> when override-request data
   is present so it can open the rich progress popover on click.
   Reset the browser's default button chrome (bg, border, padding,
   font) so it still reads as a `.pill-icon` inside the pill, and
   add a pointer cursor + subtle hover lift so the click affordance
   is discoverable. */
button.result-override-icon {
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  color: currentColor;
  cursor: pointer;
}
button.result-override-icon:hover { opacity: 0.85; }
button.result-override-icon:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

/* ── OverrideRequestCard — click-anchored popover composing the
   `.profile-popover` anatomy (same component family as the student/
   examiner avatar popover): brand-tinted header carrying entity
   identity (avatar + name + eyebrow), a 2-col key/value info grid
   for the structured fields, and a footer action. Sharing the
   shell means the override card inherits any future profile-
   popover chrome change automatically. Only the entity-specific
   pieces (eyebrow, initials avatar, close button, footer link)
   live in `.ovr-card-*` hooks below. */
.ovr-card { text-align: start; }
/* Title-only header — no avatar / no name slot since the popover
   is metadata about a request, not about a person. The entity
   identity (who last updated, who submitted) lives in the body
   info rows. */
.ovr-card-title {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
}
/* Status pill inside a 2-col info row — content-sized so it hugs
   the label rather than stretching across the value column. The
   `.profile-info-val` wrapper provides the slot; we just keep the
   pill from filling it. The row override also restores `overflow:
   visible` so the pill (with its own border + bg) doesn't get
   ellipsis-clipped by `.profile-info-val`'s nowrap-overflow rules. */
.profile-info-row .pill.is-compact { align-self: center; }
.profile-info-val:has(.pill) { overflow: visible; white-space: normal; }
/* Last Update row value — inline avatar (24px circle, initials)
   followed by name + timestamp stacked. Compact enough to fit the
   2-col grid value column at label tier; `data-tip` carries the
   full text in case the row truncates. */
.ovr-card-last-update-val {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  overflow: visible;
  white-space: normal;
}
.ovr-card-last-update-avatar {
  inline-size: 30px;
  block-size: 30px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-tint);
  color: var(--brand-700);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  flex-shrink: 0;
}
.ovr-card-last-update-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-inline-size: 0;
}
.ovr-card-last-update-name {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ovr-card-last-update-time {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* Close × button in the header — positioned on the trailing edge,
   neutral chrome that lights up on hover. */
.ovr-card-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 24px;
  block-size: 24px;
  border-radius: var(--r-sm);
  border: 0;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  margin-inline-start: auto;
  align-self: flex-start;
}
.ovr-card-close:hover { background: var(--bg-sunken); color: var(--ink); }
.ovr-card-close:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
/* `.ovr-card-foot-link` shares its visual shell with
   `.profile-foot-link` — see the combined rule earlier in this
   file. The class stays on the element only as a callsite hook. */

/* Result cell still needs overflow:visible so the outlined-pill's
   tooltip pseudo-element isn't clipped by the data-table's default
   overflow:hidden on td. */
.data-table td.cell-result { overflow: visible; }
/* Tooltip positioning handled by the unified `.data-table tbody
   [data-tip]::after` rule below. */

/* Compact mode hides the vehicle-column dot. The header already
   carries a car icon and the plate's "M"/"A" suffix encodes
   manual/automatic, so the per-row dot is redundant noise in dense
   rows where every pixel matters. */
.density-compact .vehicle-tag { gap: 0; }
.density-compact .vehicle-tag::before { display: none; }

/* Table footer */
.table-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  background: var(--bg-sunken);
}
.table-count {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  letter-spacing: 0.04em;
}
.page-size {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}
.page-size select {
  appearance: none;
  -webkit-appearance: none;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 4px 24px 4px 10px;
  font-family: inherit;
  font-size: var(--fsz-label);
  color: var(--ink);
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%), linear-gradient(-45deg, transparent 50%, var(--ink-3) 50%);
  background-position: calc(100% - 12px) 50%, calc(100% - 8px) 50%;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
}
[dir="rtl"] .page-size select { padding: 4px 10px 4px 24px; background-position: 12px 50%, 8px 50%; }
.page-size select:hover { border-color: var(--line-strong); }
.page-size select:focus { outline: 2px solid var(--ring); outline-offset: 1px; }
.pagination {
  display: flex; align-items: center; gap: 2px;
  font-size: var(--fsz-label);
}
.page-btn {
  /* sm icon-only tier — pagination chrome. See tokens.css `--cta-h-*`. */
  min-width: var(--cta-h-sm);
  height: var(--cta-h-sm);
  padding: 0 8px;
  display: inline-grid; place-items: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.page-btn:hover { background: var(--brand-tint); color: var(--ink); }
.page-btn.is-active {
  background: var(--brand-tint-2);
  color: var(--brand-700);
  border-color: color-mix(in oklch, var(--brand-500) 30%, var(--line));
}
[data-theme="dark"] .page-btn.is-active { color: var(--brand-700); }
.page-btn-nav { color: var(--ink-3); font-size: var(--fsz-h3); padding: 0 6px; }
.page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.page-btn:disabled:hover { background: transparent; color: var(--ink-3); }
.page-btn-ellipsis { font-weight: 700; letter-spacing: 1px; color: var(--ink-3); }
.page-btn-ellipsis.is-open { background: var(--bg-sunken); color: var(--ink); }

.ellipsis-wrap { position: relative; }
.ellipsis-menu {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  padding: 4px;
  max-height: 220px;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 2px;
  min-width: 220px;
  z-index: 60;
  animation: dropdown-in 120ms ease-out;
}
.ellipsis-menu-item {
  min-width: 32px;
  height: 28px;
  border: 0;
  background: transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
}
.ellipsis-menu-item:hover { background: var(--brand-tint); color: var(--brand-700); }
[data-theme="dark"] .ellipsis-menu-item:hover { color: var(--brand-700); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Disabled buttons must not respond to hover — keep their resting paint
   regardless of variant (primary/secondary/text). */
.btn:disabled:hover,
.btn-primary:disabled:hover,
.btn-secondary:disabled:hover,
.btn-text:disabled:hover,
.btn-ghost:disabled:hover {
  background: inherit;
  color: inherit;
  border-color: inherit;
  box-shadow: var(--sh-xs);
}
.btn-primary:disabled,
.btn-primary:disabled:hover { background: var(--brand-cta); color: var(--brand-ink); }
.btn-secondary:disabled,
.btn-secondary:disabled:hover { background: var(--bg-raised); color: var(--ink-2); border-color: var(--line); }
.btn-text:disabled,
.btn-text:disabled:hover { background: transparent; color: var(--ink-2); border-color: transparent; box-shadow: none; }

/* Button variants matching the team's `Button` component API */
.btn-secondary {
  background: var(--bg-raised);
  color: var(--ink-2);
  border-color: var(--line);
}
.btn-secondary:hover { background: var(--bg-sunken); border-color: var(--line-strong); color: var(--ink); }
/* Dark-mode override — the default light-theme rule recesses the
   button on hover (`bg-sunken` at L=0.14 is DARKER than `bg-raised`
   at L=0.23), which in dark mode reads as "button being pushed in"
   instead of "lifted". Resting also blends into the page bg. Bump
   both resting and hover to lighter surfaces so the secondary CTA
   reads as a defined affordance and hover ELEVATES it. */
[data-theme="dark"] .btn-secondary {
  /* Targets bumped (0.36→0.43, 0.42→0.48) for the same reason as the
     filter / icon-button uplift above — Option-B surface lift moved
     `--bg-raised` to 0.295, the old targets sat too close. */
  background: color-mix(in oklab, var(--bg-raised) 60%, oklch(0.43 0.012 var(--brand-h)));
  border-color: color-mix(in oklab, var(--line) 50%, oklch(0.48 0.016 var(--brand-h)));
}
[data-theme="dark"] .btn-secondary:hover {
  background: color-mix(in oklab, var(--brand-500) 18%, oklch(0.40 0.010 var(--brand-h)));
  border-color: color-mix(in oklab, var(--brand-500) 40%, var(--line));
  color: var(--ink);
}
.btn-text {
  background: transparent;
  color: var(--ink-2);
  border-color: transparent;
}
.btn-text:hover { background: var(--brand-tint); color: var(--ink); }
/* Destructive / cautionary variant — used by confirm modals where
   the action is reversible but consequential (e.g. deactivating a
   template stops scheduled deliveries). Uses the system error
   token rather than a fresh hue so the warning ramp stays
   consistent with the failure-rate red elsewhere. */
.btn-danger {
  background: var(--err);
  color: var(--brand-ink, white);
  border-color: var(--err);
}
.btn-danger:hover {
  background: color-mix(in oklab, var(--err) 80%, black);
  border-color: color-mix(in oklab, var(--err) 80%, black);
}
.btn-danger:disabled,
.btn-danger:disabled:hover {
  background: color-mix(in oklab, var(--err) 60%, var(--bg-sunken));
  border-color: transparent;
  color: var(--brand-ink, white);
  opacity: 0.7;
}
.btn-label { display: inline-flex; align-items: center; }
.btn-spinner {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: btn-spin 600ms linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

/* ─────────── Compact Institute Strip (scroll-triggered) ───────── */
/* Fixed slim bar that slides into view once the page-header scrolls
   out. Sits directly under the TopNav (top: 64px) and spans the full
   viewport width. The parent toggles `.is-visible` via an
   IntersectionObserver watching a sentinel at the top of .main. */
.compact-strip {
  position: fixed;
  /* With the topbar removed (sidenav now on the left), the strip
     anchors to the viewport top edge and starts AFTER the sidenav
     width — `--rail-offset` is the live sidenav width, switched
     across breakpoints (256px desktop / 64px tablet auto-rail /
     64px when manually collapsed / 0 on mobile). Same variable
     drives the institute switcher rail offset for consistency. */
  inset-block-start: 0;
  inset-inline-start: var(--rail-offset, var(--sidenav-w-full));
  inset-inline-end: 0;
  height: 40px;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--line);
  box-shadow: var(--sh-xs);
  z-index: 45;
  transform: translateY(-110%);
  transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
              inset-inline-start 200ms var(--sidenav-easing-out, ease);
  pointer-events: none;
}
/* On L2 (Institute) / L3 (Branch) pages the institute is fixed by
   the URL — the trailing cluster is now an info-only chip rather
   than a switcher CTA (see CompactInstituteStrip render: no `onSwitch`
   prop means render `.compact-strip-info` instead of the button).
   The chip is orientation only — "this is the school / branch you
   are viewing" — with no hover, no cursor, no chevron. */
/* On mobile, sit below the mobile-top-strip (48px) instead of
   covering it. The sidenav becomes a drawer (--rail-offset=0)
   so the strip already spans the full viewport width — only the
   block-start anchor needs adjustment. */
@media (max-width: 720px) {
  .compact-strip { inset-block-start: var(--mobile-strip-h); }
}
.compact-strip.is-visible {
  transform: translateY(0);
  pointer-events: auto;
}
.compact-strip-inner {
  height: 100%;
  width: 100%;
  max-width: 1600px;
  margin: 0 auto;
  padding-inline: 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
/* Wrap for the eyebrow + title — inline (single 40px-strip line)
   instead of the page-header's stacked layout, so the slim strip
   height isn't bloated. The eyebrow + middot prefix the title. */
.compact-strip-title-row {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 1 1 auto;
}
/* Inline eyebrow chip inside the compact strip — module-group
   orientation ("YARD TEST · Saved Tests") shown when scrolled past
   the page-header. Caption-size uppercase to match the page-header
   eyebrow, with a middot separator on its trailing edge so the
   chip + title read as one continuous label. */
.compact-strip-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  line-height: 1;
  white-space: nowrap;
}
.compact-strip-eyebrow svg { opacity: 0.9; }
.compact-strip-eyebrow::after {
  content: '·';
  margin-inline-start: 6px;
  font-weight: 600;
  color: var(--ink-4);
  letter-spacing: 0;
}
/* Below 540px the strip is tight — hide the eyebrow text, keep the
   icon as a silent affordance. Title gets the full width back. */
@media (max-width: 540px) {
  .compact-strip-eyebrow > span { display: none; }
  .compact-strip-eyebrow::after { display: none; }
}
/* Page title on the leading edge — mirrors the .page-h1 from the
   page-header, just smaller. */
.compact-strip-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* School-switcher cluster on the trailing edge — same affordances
   as the page-header's `.school-switcher` (mark + name + dimmed
   "switch" hint that brightens on hover), just tighter padding so
   it fits a 40px strip. */
.compact-strip-switcher {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 4px;
  max-width: min(420px, 60vw);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  cursor: pointer;
  font-family: inherit;
  color: var(--ink-1);
  transition: background var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast);
  -webkit-tap-highlight-color: transparent;
}
[dir="rtl"] .compact-strip-switcher { padding: 4px 4px 4px 10px; }
.compact-strip-switcher:hover {
  background: var(--bg-page);
  border-color: var(--line);
  box-shadow: var(--sh-xs);
}
.compact-strip-mark {
  /* Sized to feel proportional to the 13.5px name next to it in a
     40px-tall strip — square-ish logos render ~20px tall, wide
     lockups render ~36px wide × ~17px tall. Flex layout so the
     img's max-width/max-height honour the box bounds. The logo is
     anchored to the inline-end of the mark box (right in LTR, left
     in RTL) so it sits flush against the institute name beside it,
     mirroring the main school-switcher behaviour. */
  width: 36px; height: 20px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-shrink: 0;
  font-size: var(--fsz-caption);
  font-weight: 700;
  color: white;
  border-radius: 0;
}
[dir="rtl"] .compact-strip-mark { justify-content: flex-start; }
.compact-strip-mark > img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  display: block;
}
/* Logo variant — naked image (no plate, no glow); matches the school-
   switcher. The base .compact-strip-mark already anchors it to the inline
   end. A dark logo can go low-contrast on dark — accepted. */
.compact-strip-mark.compact-strip-mark--logo {
  background: transparent;
  border-radius: 0;
  box-shadow: none;
}
.compact-strip-mark > span {
  padding: 2px 6px;
  border-radius: 6px;
  background: oklch(0.55 0.12 240);
}
.compact-strip-mark[data-tone="A"] > span { background: oklch(0.55 0.16 240); }
.compact-strip-mark[data-tone="B"] > span { background: oklch(0.60 0.17 18); }
.compact-strip-mark[data-tone="C"] > span { background: oklch(0.55 0.13 160); }
.compact-strip-mark[data-tone="D"] > span { background: oklch(0.50 0.16 285); }
.compact-strip-mark[data-tone="E"] > span { background: oklch(0.55 0.14 205); }
.compact-strip-mark[data-tone="F"] > span { background: oklch(0.65 0.14 45); }
.compact-strip-mark[data-tone="G"] > span { background: oklch(0.58 0.14 325); }
.compact-strip-mark[data-tone="H"] > span { background: oklch(0.55 0.12 185); }
.compact-strip-mark[data-tone="I"] > span { background: oklch(0.55 0.12 120); }
.compact-strip-mark[data-tone="J"] > span { background: oklch(0.55 0.16 255); }
.compact-strip-mark[data-tone="K"] > span { background: oklch(0.45 0.07 35); }
.compact-strip-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  color: var(--ink-1);
}
.compact-strip-hint {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  white-space: nowrap;
  flex-shrink: 0;
  opacity: 0.7;
  transition: color var(--t-fast), opacity var(--t-fast);
}
.compact-strip-switcher:hover .compact-strip-hint {
  color: var(--ink-2);
  opacity: 1;
}
/* Info-only chip — used on L2 / L3 where the institute is pinned by
   the URL. Same icon + name layout as `.compact-strip-switcher` so
   the strip's visual rhythm stays consistent across pages, but a
   plain inline span: no button background, no hover state, no
   chevron, no cursor change. Reads as "you are viewing X". */
.compact-strip-info {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  max-width: min(420px, 60vw);
  font-family: inherit;
  color: var(--ink-1);
  cursor: default;
  -webkit-tap-highlight-color: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .compact-strip { transition: none; }
}

/* ─────────── PAGE HEADER (unified across all pages) ─────────── */
.page-header {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Negative bottom margin tightens the gap to the filter-bar below
     (which still gets the .main row-gap of 12px). Net 6px gap. */
  margin-bottom: -6px;
}
/* Top row: text-block (h1 + subtitle) on the start side, school-switcher
   pinned to the end side. Wraps on narrow viewports so the switcher drops
   below the title block instead of squeezing it. */
.page-header-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
/* Inline-eyebrow header (Saved Tests / Reports / Live): the eyebrow now
   shares the title's row, so the text block is shorter than the institute
   switcher beside it. Center the row so the header and the switcher sit at
   the SAME LEVEL — instead of the switcher drooping below a top-aligned,
   now-single-row header — which also reclaims most of the empty space that
   was left under the title. Then tighten the gap to the content below a
   touch. Scoped via :has so non-inline headers keep their top alignment. */
.page-header-top:has(.page-header-text--eyebrow-inline) {
  align-items: center;
}
.page-header:has(.page-header-text--eyebrow-inline) {
  margin-bottom: -9px;
}
/* The stacked institute switcher sets align-self:flex-start + a 4px bottom
   margin (for the default top-aligned header); in the centered inline-eyebrow
   row that lifts it ~2px above the title line. Center it and drop the bottom
   margin so its box shares the same middle line — the treatment the
   video-wall header already uses on this same component. */
.page-header-top:has(.page-header-text--eyebrow-inline) .school-switcher {
  align-self: center;
  margin-block-end: 0;
}
.page-header-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 auto; }
/* MOBILE only: put the eyebrow INLINE as a prefix to the title (with a
   trailing vertical separator) to save vertical space — the school-
   switcher wraps below it anyway. Desktop/tablet keep the stacked
   eyebrow-above-title hierarchy (its documented orientation role).
   Matches the video-wall / reports inline pattern. */
@media (max-width: 720px) {
  .page-header-text {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 0;
    row-gap: 4px;
  }
  .page-header-text > .page-h1-eyebrow {
    margin-block-end: 0;
    margin-inline-end: 10px;
    padding-inline-end: 12px;
    border-inline-end: 1px solid var(--line);
  }
  .page-header-text > .page-h1-sub { flex-basis: 100%; }
}
/* Opt-in INLINE eyebrow (PageHeader `eyebrowInline` prop) — puts the
   eyebrow on the SAME ROW as the title, split by a hairline vertical
   divider, instead of the default stacked eyebrow-above-title. The
   eyebrow keeps its NORMAL size/weight (only the layout changes; the
   video-wall divider treatment without shrinking the type). Applies at
   ALL widths (the mobile rule above already inlines every page's eyebrow
   ≤720; this extends that to desktop). Used on the PageHeader pages
   (Saved Tests / Reports / Live). The sub-line drops to its own row so
   the title row stays clean. */
.page-header-text--eyebrow-inline {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0;
  row-gap: 4px;
}
.page-header-text--eyebrow-inline > .page-h1-eyebrow {
  margin-block-end: 0;
  margin-inline-end: 8px;
  padding-inline-end: 8px;
  border-inline-end: 1px solid var(--line);
}
/* Optically center the eyebrow icon on the uppercase CAPS, not the text
   box. The label has no descenders, so its line box carries ~2px of empty
   space below the glyphs — which makes a box-centered icon read low. This
   2px upward nudge is paint-only (position offset; the layout box is
   unchanged, so the row-level centering with the title stays intact) and
   sits the full-size icon on the caps' true middle. */
.page-header-text--eyebrow-inline > .page-h1-eyebrow svg {
  position: relative;
  inset-block-start: -2px;
}
/* Title + scope chip onto the same middle line as the eyebrow. The h1
   defaults to baseline-align, which drops the chip pill low; centering it
   makes the eyebrow, title and pill all read on one shared middle line. */
.page-header-text--eyebrow-inline > .page-h1 {
  align-items: center;
}
.page-header-text--eyebrow-inline > .page-h1-sub { flex-basis: 100%; }
.page-h1 {
  margin: 0;
  font-size: var(--fsz-h1);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.2;
}
[dir="rtl"] .page-h1 { letter-spacing: 0; }
/* Eyebrow / kicker label above the page H1. Used to surface the
   module-group context (e.g., "YARD TEST" above "Saved Tests") so
   users — especially on mobile or in shared screenshots where the
   sidenav isn't visible — can tell at a glance whether they're on
   a Yard Test or Road Test surface. Caption-sized, uppercase, muted
   ink so it reads as orientation chrome and never competes with the
   actual page title. */
.page-h1-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;  /* icon ↔ label — 6px reads tighter than 8 at this small, letter-spaced scale */
  margin-block-end: 6px;
  font-size: var(--fsz-body);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  line-height: 1;
}
.page-h1-eyebrow svg { opacity: 0.9; }
/* Scope chip inside the page h1 — small pill with a lock glyph
   that signals the user is viewing a role-restricted subset of
   the data (e.g., "Your branch only" / "Your sector"). Sits
   inline with the title so the chip and h1 read as one unit. */
.page-h1-scope {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-inline-start: 10px;
  vertical-align: middle;
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  color: var(--brand-700);
  white-space: nowrap;
}
.page-h1-scope svg { opacity: 0.85; }
/* Caption-tier uppercase variant for the page subtitle — used when
   the subtitle carries operational counts ("9 PENDING · 6 COMPLETED")
   rather than a sentence. Renders as a child span inside the standard
   `.page-h1-sub` div, overriding the body-tier shorthand. Matches the
   `.ltp-queue-sub` pattern from the draft live-tests prototype. */
.page-h1-sub-counts {
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--ink-3);
}

.page-h1-sub {
  font-size: var(--fsz-body);
  color: var(--ink-3);
  line-height: 1.5;
  max-width: 720px;
}
[dir="rtl"] .page-h1-sub { font-size: var(--fsz-body); }

/* Inline-subtitle variant — `.page-h1-sub--inline` is a span rendered
   INSIDE the .page-h1 element on the same row as the title. The h1
   uses inline-flex with baseline alignment so the title and subtitle
   share a baseline. Margin-inline-start gives breathing room; on
   mobile (≤720px) the inline span is flipped to a stacked block so
   the row doesn't crowd the school-switcher offscreen. */
.page-h1 { display: inline-flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.page-h1-sub--inline {
  display: inline;
  margin-inline-start: 0;
  font-size: var(--fsz-body);
  color: var(--ink-3);
  line-height: 1.5;
  max-width: none;
}
@media (max-width: 720px) {
  .page-h1-sub--inline {
    display: block;
    flex-basis: 100%;
    margin-inline-start: 0;
    margin-block-start: 4px;
  }
}

/* ─────────── TABS — underline-style page tabs ─────────── */
.tabs {
  display: flex;
  align-items: center;
  gap: 0;
  border-bottom: 1px solid var(--line);
  margin-top: 4px;
  /* Allow the row to scroll horizontally when the labels don't fit
     on narrow viewports (mobile / split layouts). The scrollbar is
     hidden so it reads as a clean overflow strip. Touch devices get
     momentum scroll for free. */
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs-spacer {
  /* Don't fight the scroll: the spacer should only expand to fill
     extra room when the tabs already fit. `flex-shrink: 1` lets it
     collapse to 0 when the row needs to scroll, so the secondary
     tab sits right after the primary group instead of being pushed
     off-screen by an over-greedy spacer. */
  flex: 1 1 0;
  min-width: 16px;
}
.tab {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 14px;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--fsz-body);
  font-weight: 500;
  cursor: pointer;
  margin-bottom: -1px;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
  white-space: nowrap;
  /* Tabs keep their natural width so labels never truncate; the
     `.tabs` container scrolls instead of letting buttons shrink and
     ellipsize their text. */
  flex-shrink: 0;
}
/* Tab hover + active aligned to the system vocabulary:
   hover  → brand-600 @ 6% (same as table rows + sidenav rows)
   active → brand-600 text (was brand-700, now matches the rest
            of the active surfaces). The bold weight + underlined
            border-bottom remain — that's the tab-specific
            "you're on this view" affordance. */
.tab:hover {
  color: var(--ink);
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
}
.tab.is-on {
  color: var(--brand-600);
  border-bottom-color: var(--brand-600);
  font-weight: 600;
}
[data-theme="dark"] .tab.is-on { color: var(--brand-600); }
/* Composes with `.pill.is-compact.is-neutral` for the base chrome.
   Adds tab-specific overrides only: min-width for round-number
   badges, tabular numerals so digit-counts line up. */
.tab-badge {
  min-inline-size: 18px;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}
/* Active-tab tint — same brand-tinted variant the tab itself
   carries, so the badge stays visually paired with its tab. */
.tab.is-on .tab-badge {
  background: var(--brand-tint);
  border-color: color-mix(in oklch, var(--brand-500) 25%, var(--line));
  color: var(--brand-700);
}
.tab--secondary { padding-inline-start: 14px; border-inline-start: 1px solid var(--line); margin-inline-start: 4px; }

/* ─────────── BREADCRUMBS ─────────── */
.crumbs {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 4px;
  font-size: var(--fsz-label);
}
[dir="rtl"] .crumbs { font-size: var(--fsz-body); }
.crumb {
  background: transparent;
  border: 0;
  padding: 4px 6px;
  border-radius: var(--r-sm);
  font-family: inherit;
  font-size: inherit;
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast);
}
.crumb:hover:not(:disabled) { color: var(--ink); background: var(--brand-tint); }
.crumb.is-current {
  color: var(--ink);
  font-weight: 600;
  cursor: default;
}
.crumb-sep { color: var(--ink-4); padding: 0 2px; }

/* ─────────── KPI STRIP ─────────── */
/* Content-aware sizing: cards are at least 140px wide; auto-fit
   packs as many as will fit in one row, then wraps to a new row.
   No viewport-band override needed — the grid responds to its
   actual container width, not a media query. */
.kpi-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px;
}
.kpi {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
  display: flex; flex-direction: column; gap: 6px;
  min-width: 0;
}
.kpi-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .kpi-label { font-size: var(--fsz-caption); letter-spacing: 0; text-transform: none; }
.kpi-value {
  font-size: var(--fsz-h1);
  font-weight: 700;
  letter-spacing: var(--ls-h1);
  color: var(--ink);
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .kpi-value { letter-spacing: 0; }
.kpi.is-pass .kpi-value { color: var(--ok); }
.kpi.is-fail .kpi-value { color: var(--err); }
/* `kpi--wrap` opt-in: lets the value span up to 2 lines and truncate
   with an ellipsis if it still doesn't fit. Used on cards whose
   value is a name (e.g. the "Maneuver" card in Test Analysis L2)
   where the headline read benefits from the full word over a clipped
   single-line ellipsis. The default behaviour for KPI values stays
   single-line nowrap so numeric headlines (like "8,261", "72.7%") read
   on one line. */
.kpi.kpi--wrap .kpi-value {
  white-space: normal;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.15;
}

/* KPI badge — pinned to the top-right (inline-end) of the card, matching
   the ResultPill placement on the L4 maneuver cards. The KPI gets
   `has-badge` to reserve right-padding on the label so long labels don't
   run under the badge. */
.kpi { position: relative; }
.kpi.has-badge .kpi-label { padding-inline-end: 72px; }
.kpi-badge {
  position: absolute;
  top: 12px;
  inset-inline-end: 12px;
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  white-space: nowrap;
}
[dir="rtl"] .kpi-badge { font-size: var(--fsz-caption); letter-spacing: 0; text-transform: none; }
.kpi-badge.is-info {
  background: var(--brand-tint);
  color: var(--brand-700);
  border-color: color-mix(in oklch, var(--brand-500) 25%, var(--line));
}
.kpi-badge.is-pass {
  background: color-mix(in oklab, var(--ok) 12%, var(--bg-raised));
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 30%, var(--line));
}
.kpi-badge.is-warn {
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 14%, var(--bg-raised));
  color: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 70%, var(--ink));
  border-color: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 35%, var(--line));
}
.kpi-badge.is-fail {
  background: color-mix(in oklab, var(--err) 10%, var(--bg-raised));
  color: var(--err);
  border-color: color-mix(in oklab, var(--err) 30%, var(--line));
}

/* KPI footer-meta — small muted text pinned to the bottom-end (right in
   LTR) of the card. Used at L4 for the score "72/100" — sits below the
   value without competing with it. */
.kpi { padding-bottom: 18px; }

/* Reports KPI cards — give the headline (label + value) and the
   bottom-pinned breakdown sub-row a guaranteed gap between them.
   `.kpi-sub` already pulls itself to the bottom via `margin-top: auto`,
   so the only way to widen the gap visually is to grow the card's
   minimum height — without it, single-line content collapses the two
   rows together. Scoped to `.rpt-doc` so the change is confined to
   reports pages. */
.rpt-doc .kpi { min-height: 104px; }
.kpi-meta {
  position: absolute;
  bottom: 8px;
  inset-inline-end: 14px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
[dir="rtl"] .kpi-meta { font-size: var(--fsz-label); }
/* Smaller trailing fragment inside a KPI value, e.g. "#1 of 3" or
   gender breakdown "120m / 80f" — keeps the headline number prominent. */
.kpi-value-sub {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
  letter-spacing: 0;
  margin-inline-start: 2px;
}
[dir="rtl"] .kpi-value-sub { font-size: var(--fsz-label); }

/* Sub-row beneath the KPI value — used for breakdowns like
   "1,425 Male · 905 Female", "12 Branches", or "Pending · Completed".
   `margin-top: auto` pins it to the bottom of the flex-column .kpi card,
   so the headline figure breathes at the top and the supporting
   breakdown sits as a quiet footer. Combined with the grid's default
   stretch alignment, all cards in a row share the same height and
   their sub-rows line up across the row. */
.kpi-sub {
  display: flex;
  /* Vertically center the icon glyphs against their numeric/text
     siblings inside the row — baseline alignment makes outlined
     SVGs (especially `male` / `female` and the new `hourglass` /
     `checkCircle`) sit slightly low because their visual centre
     doesn't match the alphabetic baseline of the digits. */
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  line-height: 1.2;
  /* Pin the breakdown row to the bottom-start of the .kpi flex
     column so every card's sub-row lines up across the strip,
     regardless of value-line wrapping above. */
  margin-top: auto;
  white-space: nowrap;
  /* No overflow clipping — the GenderSeg tooltip is a CSS ::after
     anchored above (and centered on) the segment, so any horizontal
     clip on this row chops the tooltip when the segment sits near
     the row's left or right edge. KPI breakdowns are short
     ("1,425 · 905", "47 Branches"), so we don't need ellipsis here. */
  overflow: visible;
}
[dir="rtl"] .kpi-sub { font-size: var(--fsz-label); }
/* Center each segment internally too so the icon and the value
   share a common vertical centerline (matches the `.kpi-sub`
   change above). */
.kpi-sub-seg { display: inline-flex; align-items: center; gap: 2px; }
/* Gender split bar — single horizontal track + tiny legend.
   Used inside .kpi-sub to replace the two GenderSeg items
   with one composition bar (matches the T2 dashboard's
   DigestAppointments split bar pattern). */
.kpi-genderbar {
  display: flex;
  flex-direction: column;
  gap: 4px;
  inline-size: 100%;
  position: relative;
}
.kpi-genderbar-track {
  display: flex;
  /* 3px gap between male and female pills so each segment reads as
     its own rounded shape rather than two halves of one bar. No
     overflow:hidden — segments carry their own border-radius. */
  gap: 3px;
  block-size: 6px;
  inline-size: 100%;
}
.kpi-genderbar-male,
.kpi-genderbar-female {
  block-size: 100%;
  /* Full pill rounding on each segment. */
  border-radius: 999px;
  transition: flex-basis 240ms ease, filter 140ms ease;
  cursor: pointer;
}
.kpi-genderbar-male:hover,
.kpi-genderbar-female:hover {
  filter: brightness(0.92);
}
.kpi-genderbar-male   { background: var(--d-male,   oklch(0.58 0.16 250)); }
.kpi-genderbar-female { background: var(--d-female, oklch(0.66 0.18 350)); }
.kpi-genderbar-legend {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}
.kpi-genderbar-leg {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.kpi-genderbar-leg b { font-weight: 600; color: var(--ink-2); }
.kpi-genderbar-dot {
  inline-size: 7px;
  block-size: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}
.kpi-genderbar-dot.is-male   { background: var(--d-male,   oklch(0.58 0.16 250)); }
.kpi-genderbar-dot.is-female { background: var(--d-female, oklch(0.66 0.18 350)); }
/* M / F letter glyph next to the legend dot — keeps the gender
   label explicit even though the dot already carries colour cue. */
.kpi-genderbar-letter {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-3);
  margin-inline-end: -1px;
}
/* ── Hover popover ── shown when a male/female segment is hovered;
   surfaces the per-gender breakdown the consumer passes via
   maleBreakdown / femaleBreakdown props. position: fixed escapes
   any overflow:hidden ancestor (e.g. report tables, panel chrome). */
/* GenderSplitBar breakdown — INNER content only. Outer chrome (bg,
   border, shadow, position, arrow, font) comes from the wrapping
   <ChartTip>. These rules style the rows/labels/values against the
   inverted tooltip surface. */
.kpi-genderbar-pop {
  display: flex;
  flex-direction: column;
  min-inline-size: 150px;
}
.kpi-genderbar-pop-head {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
  font-size: var(--fsz-caption);
  color: var(--tooltip-fg);
  padding-block-end: 6px;
  margin-block-end: 6px;
  border-block-end: 1px solid var(--tooltip-border);
  inline-size: 100%;
  letter-spacing: 0.02em;
}
.kpi-genderbar-pop-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.kpi-genderbar-pop-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: var(--fsz-caption);
  color: var(--tooltip-fg-muted);
}
.kpi-genderbar-pop-row-label {
  color: var(--tooltip-fg-muted);
}
.kpi-genderbar-pop-row-value {
  font-weight: 600;
  color: var(--tooltip-fg);
}
/* Tone-coded values keep their semantic color even on the inverted
   surface — bright enough at full --ok / --err / --warn intensity to
   read against the dark bg. */
.kpi-genderbar-pop-row.is-ok    .kpi-genderbar-pop-row-value { color: var(--ok); }
.kpi-genderbar-pop-row.is-err   .kpi-genderbar-pop-row-value { color: var(--err); }
.kpi-genderbar-pop-row.is-warn  .kpi-genderbar-pop-row-value { color: var(--warn); }
.kpi-genderbar-pop-row.is-muted .kpi-genderbar-pop-row-value { color: var(--tooltip-fg-muted); }
.kpi-sub-num {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  /* Sub-row is supporting context for the headline value above it —
     drop a step on the ink ramp (ink-2 → ink-3) so the breakdown
     reads as quieter dark-gray rather than competing near-black. */
  color: var(--ink-3);
}
.kpi-sub-tag {
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  letter-spacing: 0.02em;
}
[dir="rtl"] .kpi-sub-tag { font-size: var(--fsz-caption); letter-spacing: 0; }
/* Gender icon inside the KPI sub-row — outlined silhouettes inlined
   in the `Icon` component, drawn through `currentColor` so they pick
   up the surrounding ink token automatically (light theme ⇒ dark ink,
   dark theme ⇒ light ink). Both genders share the same ink-3 muted
   tone — the figure shape itself is the differentiator, not colour,
   which keeps the design system's palette consistent. */
.kpi-sub-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* No directional margin — the parent `.kpi-sub-seg gap: 2px` is the
     sole source of icon↔value spacing, so the gap is exactly 2px in
     both LTR and RTL. */
  color: var(--ink-3);
  /* Optical-centering nudge. Flex `align-items: center` aligns the
     icon's 12×12 box against the number's line-box, but digits have
     no descenders so their visual centerline sits ~1px above the
     line-box geometric center. Shift the icon up by 1px so its
     optical center matches the numerals' rather than the line-box's. */
  transform: translateY(-1px);
}
[data-theme="dark"] .kpi-sub-icon { color: var(--ink-3); }
.kpi-sub-icon svg {
  /* The asset is tall (211:310 ≈ 0.68:1) and is an outlined glyph
     (stroke only, no fill, stroke-width 32 in viewBox units). The Icon
     component sets the SVG box to a square via inline width/height
     (controlled by the `size` prop in GenderSeg), and the default
     preserveAspectRatio centers the figure inside that box at its
     true aspect ratio. So `size=12` yields a ~8.2px-wide × 12px-tall
     figure with a ~1.24px stroke — visually balanced beside the
     12.5px KPI sub numbers. */
  display: block;
  overflow: visible;
}
.kpi-sub-seg[data-tip] { cursor: default; }
.kpi-sub-sep { color: var(--ink-4); }
.kpi-sub-plain { color: var(--ink-3); }

/* ─────────── PASS-RATE CELL & FAIL-RATE BAR ─────────── */
.pr-cell {
  /* Number stacks ABOVE the bar (top-left) — matches the
     RateBarCell / overrides-cell layout for consistency across
     every pass-rate / fail-rate column in the app. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  min-width: 84px;
  /* Tooltip needs a positioning context. */
  position: relative;
}
/* The td containing a tooltip-bearing PrCell must opt out of the
   table-wide `overflow: hidden` so the floating breakdown card can
   rise above the row. Other cells (vehicle, result, action cells)
   have their own opt-out classes; PrCell uses `:has()` so any td
   that ends up holding one inherits the opt-out automatically,
   without each call site having to remember to add a class. */
.data-table td:has(.pr-cell--has-tip) { overflow: visible; }
.pr-cell--has-tip { cursor: default; }
.pr-bar {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
}
/* Split bar — the track itself represents fail share (red); the fill on top
   represents pass share (green). Reading left-to-right the user sees the
   green/red border land exactly at the percentage. */
.pr-bar--split {
  background: color-mix(in oklab, var(--err) 28%, var(--bg-sunken));
}
/* Inverted variant — track is green (pass share), fill is red (fail
   share). Used on failure-rate columns so the red portion reads from the
   bar's leading edge (where the eye lands first). */
.pr-bar--inverted {
  background: color-mix(in oklab, var(--ok) 28%, var(--bg-sunken));
}
.pr-fill {
  height: 100%;
  border-radius: 3px;
  transition: width 200ms ease;
}
.pr-fill--pass { background: var(--ok); }
.pr-fill--fail { background: var(--err); }
.pr-num {
  /* Number on top-left of the bar (column-stacked layout).
     Per the table-typography rule: only the first (identity)
     column is bold. The pass-rate % above the bar is regular
     weight at the table's content size — same as plain numeric
     cells. The bar carries the visual; the number carries the
     value; identity remains the only bold element on the row. */
  align-self: flex-start;
  font-size: var(--fsz-body);
  font-weight: 400;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1;
}
/* PrCell breakdown popover content — INNER LAYOUT ONLY.
   Outer chrome (bg, border, shadow, position, arrow, font) comes from
   the wrapping `.chart-tip` (PrCell renders this through the unified
   ChartTip portal). This class controls only the 4-column grid
   layout of the breakdown rows (Label | Overall | Male | Female). */
/* Same shape as the dashboard's `.rate-cell-pop-row` — a colored
   dot prefixes each data row so the legend (pass = green, fail =
   red) reads at a glance without parsing labels. 5 columns now:
   dot | label | overall | male | female. */
.pr-cell-tip {
  display: grid;
  grid-template-columns: 8px max-content max-content max-content max-content;
  gap: 4px 12px;
  align-items: center;
}
.pr-cell-tip-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 999px;
  align-self: center;
}
.pr-cell-tip-row.is-pass .pr-cell-tip-dot { background: var(--pass); }
.pr-cell-tip-row.is-fail .pr-cell-tip-dot { background: var(--fail); }

.fail-bar { display: flex; align-items: center; gap: 8px; min-width: 100px; }
.fail-bar-track {
  flex: 1;
  height: 6px;
  background: var(--bg-sunken);
  border-radius: 3px;
  overflow: hidden;
}
.fail-bar-fill {
  height: 100%;
  background: color-mix(in oklab, var(--err) 70%, transparent);
  border-radius: 3px;
}
.fail-bar-num {
  /* Numeric data cell — regular weight per the table-typography
     rule (only identity column is bold). */
  font-size: var(--fsz-label);
  font-weight: 400;
  color: var(--err);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  min-width: 48px;
  text-align: end;
}

/* ─────────── SEVERITY PILL (Minor / Major) ─────────── */
.sev-pill {
  display: inline-flex; align-items: center;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  font-weight: 500;
  border: 1px solid transparent;
  white-space: nowrap;
}
/* Removed: was bumping pill font-size in RTL, breaking the
   11px caption-tier consistency that all sibling pills share. */
.sev-pill.is-minor {
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 14%, transparent);
  color: oklch(0.5 0.13 75);
  border-color: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 35%, var(--line));
}
[data-theme="dark"] .sev-pill.is-minor { color: oklch(0.78 0.12 75); }
.sev-pill.is-major {
  background: color-mix(in oklab, var(--err) 12%, transparent);
  color: var(--err);
  border-color: color-mix(in oklab, var(--err) 35%, var(--line));
}

/* Schedules — Report cell stacks the report name with the run-cadence
   string ("Every Monday · 08:00") as a quiet subtitle. Replaces the
   standalone Frequency column so the row reads at a glance: what is
   it, and how often does it run? */
/* Match the Custom Reports template-table row height + cell font
   metrics so Custom Reports and Schedules read as siblings. The
   `.sched-table` (no density-cozy class on its parent) would
   otherwise size rows to content and end up shorter/uneven. */
.sched-table-wrap .sched-table tbody td,
.sched-table-wrap .sched-table tbody tr { height: 56px; }
.sched-report-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.sched-report-cell .cell-strong {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[dir="rtl"] .sched-report-cell .cell-strong { font-size: var(--fsz-body); }
.sched-report-freq {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  /* Same line-height as `.tpl-desc` so the two-line cell sums to
     the same vertical reach in both tables. */
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[dir="rtl"] .sched-report-freq { font-size: var(--fsz-label); }

/* ─────────── STATUS PILL (unified) ─────────── */
/* Single shared pill for every status surface in the app. Replaces
   the per-feature `.sched-status` and inline `.tpl-status` styles so
   the visual treatment (shape, dot size, padding, color ramp) stays
   identical across Custom Reports, Schedules, and any future feature
   that needs a status indicator (requests, tests, vehicles). */
/* StatusPill composes the unified `.pill` primitive (see top of
   file) — chrome, padding, dot, dark-mode contrast are all
   inherited from there. The only callsite-specific override is the
   dashed border on `.is-draft`: it differentiates "not yet
   committed" from `.is-paused` (which is amber) by switching to a
   dashed border in muted ink — same visual shorthand the filter
   pills use for "pending change". Reads as informational, not
   alarming, and never collides with Paused at a glance. */
.pill.status-pill.is-draft {
  border-style: dashed;
  border-color: var(--line-strong);
}

/* ─────────── CALLOUT BANNER ─────────── */
.callout {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 14px;
  background: var(--brand-tint);
  border: 1px solid color-mix(in oklch, var(--brand-500) 22%, var(--line));
  border-radius: var(--r-md);
  color: var(--ink);
  font-size: var(--fsz-label);
  line-height: 1.5;
}
[dir="rtl"] .callout { font-size: var(--fsz-body); }
.callout-icon {
  width: 26px; height: 26px;
  display: grid; place-items: center;
  background: var(--bg-raised);
  border-radius: 50%;
  color: var(--brand-700);
  flex-shrink: 0;
}
[data-theme="dark"] .callout-icon { color: var(--brand-700); }
.callout-text { padding-top: 3px; }
.callout-text strong { font-weight: 600; color: var(--ink); }
.callout-text em { font-style: normal; font-weight: 600; color: var(--brand-700); }
.callout-text .em { font-weight: 600; color: var(--err); }
[data-theme="dark"] .callout-text em { color: var(--brand-700); }

/* ─────────── REPORT DOC HEAD ─────────── */
/* Two-row layout:
     row 1 — title (left) + action buttons (right)
     row 2 — level chain spanning the full head width
   Stacking lets the chain use the whole card before wrapping, instead
   of competing with the actions for horizontal space. */
.rpt-head {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px 18px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-xs);
}
.rpt-head-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  /* Container query scope: when this row gets narrow enough that the
     title would need a 2nd line (title text + "X levels" badge no
     longer fit on a single line), we also stack the action CTAs
     vertically — see `@container rpt-head-row` below. Using a
     container query (rather than a viewport media query) keeps the
     stack tied to the actual row width, which is what governs whether
     the title wraps in the first place. */
  container-type: inline-size;
  container-name: rpt-head-row;
}
.rpt-head-info { min-width: 0; flex: 1; }
.rpt-head-kind {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--ink-4);
  text-transform: uppercase;
}
[dir="rtl"] .rpt-head-kind { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.rpt-head-title {
  font-size: var(--fsz-h2);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-top: 4px;
  line-height: 1.25;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
[dir="rtl"] .rpt-head-title { letter-spacing: 0; }
/* Composes with `.pill.is-compact.is-neutral` for chrome. Adds
   head-title-specific overrides: tabular numerals + no letter
   spacing (the title has tracking, the badge shouldn't inherit it),
   and white-space: nowrap so the badge stays on a single line. */
.rpt-head-title-badge {
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.rpt-head-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 4px 24px;
  margin: 12px 0 0;
}
.rpt-head-grid > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rpt-head-grid dt {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
  margin: 0;
}
[dir="rtl"] .rpt-head-grid dt { font-size: var(--fsz-caption); letter-spacing: 0; text-transform: none; }
.rpt-head-grid dd {
  font-size: var(--fsz-label);
  color: var(--ink);
  margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .rpt-head-grid dd { font-size: var(--fsz-body); }
.rpt-head-actions {
  display: inline-flex; align-items: center; gap: 6px;
  flex-shrink: 0;
}

/* Mobile: reorder so the LevelChain shows BEFORE the action buttons.
   On desktop the row layout (title left, actions right, level-chain
   row below) reads naturally; on mobile everything stacks and the
   default DOM order puts the buttons BEFORE the level chain, which
   buries the "which level am I on" affordance and pushes the user's
   first interactive choice into a less useful generate/schedule
   action. `display: contents` collapses `.rpt-head-row` so its
   children participate in `.rpt-head`'s flex layout directly,
   letting us reorder with `order`. */
@media (max-width: 720px) {
  .rpt-head-row { display: contents; }
  .rpt-head-info { order: 0; }
  .rpt-head .rpt-level-chain-wrap { order: 1; }
  .rpt-head-actions { order: 2; align-self: stretch; }
  /* Previously: `.rpt-head-actions > * { flex: 1 1 0 }` stretched
     each button to fill the row. Dropped — CTAs keep their
     intrinsic content width across viewports (per the global
     `.btn { flex: 0 0 auto }` rule). The actions row right-aligns
     them via flex defaults. */
}

/* When the head row narrows past the point where the title can keep
   its name + "X levels" badge on one line (~560px of row width),
   stack the action CTAs vertically. Using `column-reverse` puts
   Generate Report on top and Schedule beneath it (visual order is
   reversed from source order, where Schedule comes first). The
   stacked column right-aligns each button at its content width so
   they don't blow up to fill the column. */
@container rpt-head-row (max-width: 559px) {
  .rpt-head-actions {
    flex-direction: column-reverse;
    align-items: flex-end;
    gap: 6px;
  }
}

/* ─────────── LEVEL CHAIN (inside rpt-head) ─────────── */
/* Drill-down navigation: replaces the previous <Breadcrumbs> that sat
   above the report tab. Sits directly under the report title in the
   head card. Each level is a pill with an "L1/L2" badge plus the
   level's label; non-current pills are buttons that drill back up the
   hierarchy when clicked, and the current pill is filled with brand
   tint and not interactive. */
.rpt-level-chain-wrap {
  /* Margin removed — the chain now sits as a sibling of `.rpt-head-row`
     inside `.rpt-head`, which is a column flex with its own `gap: 10px`.
     Adding margin here would double the spacing between rows. */
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Stretch the chain and hint to the wrap container's full width
     (which is now the full `.rpt-head` width since we hoisted the
     wrap out of `.rpt-head-info`). Gives the chain its full flex-wrap
     budget — pills wrap only when they truly can't fit on one line. */
  align-items: stretch;
}
.rpt-level-chain {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
}
.rpt-level-pill { display: inline-flex; }
.rpt-level-pill-inner {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px 3px 3px;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
  white-space: nowrap;
}
[dir="rtl"] .rpt-level-pill-inner { padding: 3px 3px 3px 10px; }
button.rpt-level-pill-inner:hover {
  /* Aligned to system hover vocabulary: brand-600 @ 6% + brand-600
     text. The border softens too (35% → 22%) so the pill doesn't
     read heavier than the rest of the system's hover state. */
  border-color: color-mix(in oklab, var(--brand-500) 22%, var(--line-2));
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--brand-600);
}
button.rpt-level-pill-inner:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.rpt-level-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--r-pill);
  background: var(--bg-sunken);
  color: var(--ink-3);
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  transition: background var(--t-fast), color var(--t-fast);
}
/* Outer-pill hover still applies (background tint on the chain
   button). The L# circle stays in its default `--bg-sunken` /
   `--ink-3` styling — shifting it to a lighter brand-tint on hover
   read as the circle losing definition, which felt wrong. The
   pill's outer hover affordance is plenty of visual feedback. */
.rpt-level-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}
/* Current level — filled, marked, non-interactive. The brand-filled
   number badge plus the dot at the end gives two redundant cues that
   this is the current view ("you are here") without relying solely
   on weight/colour. */
.rpt-level-pill.is-current .rpt-level-pill-inner {
  /* Aligned to system active vocabulary: brand-500 @ 12% pill +
     brand-600 text. The 22% border keeps the pill defined against
     the surrounding hairlines. */
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 22%, var(--line-2));
  color: var(--brand-600);
  cursor: default;
}
.rpt-level-pill.is-current .rpt-level-num {
  background: var(--brand-600);
  color: var(--brand-ink);
}
/* Current-pill indicator dot — rendered as a real DOM <span> by
   LevelChain (not an ::after pseudo). The pseudo-element approach
   collided with the global [data-tip]::after tooltip system on the
   clickable-current pill (which carries data-tip="Change L2"); the
   indicator inherited the tooltip rule's `position: absolute` and
   floated as a stranded brand-coloured square. */
.rpt-level-pill.is-current .rpt-level-pill-inner .rpt-level-pill-dot {
  display: inline-block;
  inline-size: 6px;
  block-size: 6px;
  border-radius: var(--r-pill);
  background: var(--brand-600);
  margin-inline-start: 2px;
  flex-shrink: 0;
}
/* When a current-level re-pick is wired up, the current pill becomes
   a clickable button — opens a popover that lets the user swap to a
   different entity at this level. Hover deepens the brand-tint so the
   interactivity reads at-a-glance; .is-open inverts to brand-fill
   while the popover is showing (matching the dashed drill trigger's
   open state grammar). */
button.rpt-level-pill-inner.is-clickable-current {
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
button.rpt-level-pill-inner.is-clickable-current:hover {
  /* Current pill is already at 12% (active vocabulary). Hovering
     it indicates "you can switch entities here" — bump one tier
     stronger (16%) so the hover reads as a clear interaction
     cue without jumping all the way to the heavy is-open state. */
  background: color-mix(in oklab, var(--brand-500) 16%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line-2));
}
button.rpt-level-pill-inner.is-clickable-current.is-open {
  background: var(--brand-600);
  color: var(--brand-ink);
  border-color: var(--brand-600);
}
button.rpt-level-pill-inner.is-clickable-current.is-open .rpt-level-num {
  background: var(--brand-ink);
  color: var(--brand-600);
}
button.rpt-level-pill-inner.is-clickable-current.is-open .rpt-level-pill-dot {
  background: var(--brand-ink);
}
button.rpt-level-pill-inner.is-clickable-current:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.rpt-level-sep {
  display: inline-flex;
  align-items: center;
  color: var(--ink-4);
  font-size: var(--fsz-h3);
  user-select: none;
  padding: 0 2px;
  list-style: none;
}
[dir="rtl"] .rpt-level-sep { transform: scaleX(-1); }
/* Affordance hint — explicitly tells the user the levels are
   navigable. Hidden when there's only one level in the chain (nothing
   to navigate to). Reuses the click-cursor glyph from the table-row
   drill hint so the "this is clickable" cue stays consistent. */
.rpt-level-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  letter-spacing: 0;
  /* Always single-line. The hint is short enough that allowing it to
     wrap looks like a typesetting bug rather than a deliberate break;
     keeping it on one line also stops it from competing visually with
     the level pills above it. */
  white-space: nowrap;
}
.rpt-level-hint > span { white-space: nowrap; }
[dir="rtl"] .rpt-level-hint { font-size: var(--fsz-label); }
.rpt-level-hint svg {
  color: var(--brand-600);
  flex-shrink: 0;
}

/* ─────────── DRILL-DOWN PICKER (tail of LevelChain) ─────────── */
/* Trigger: small text-button styled to read as "next action available"
   without competing with the level pills themselves. Sits at the tail
   of the chain after a › separator. Open state inverts the colour to
   give a clear "popover is showing" signal. */
.rpt-level-drill-tail {
  display: inline-flex;
  position: relative;
}
.rpt-drill-down {
  position: relative;
  display: inline-flex;
}
.rpt-drill-down-trigger {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  /* Text aligned to brand-600 (system active vocabulary). The
     dashed brand-tinted border stays — it's the deliberate
     "this is an invitation to drill down" affordance that
     distinguishes the hint from the solid level pills. */
  color: var(--brand-600);
  background: transparent;
  border: 1px dashed color-mix(in oklab, var(--brand-500) 45%, var(--line));
  border-radius: var(--r-pill);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
[dir="rtl"] .rpt-drill-down-trigger { font-size: var(--fsz-label); }
.rpt-drill-down-trigger:hover {
  /* Hover bg aligned to system: brand-600 @ 6%. Border switches
     dashed → solid on hover to signal "armed", and softens
     from 50% to a less saturated mix that's consistent with
     the rest of the hover-state borders in the system. */
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  border-style: solid;
  border-color: color-mix(in oklab, var(--brand-500) 35%, var(--line));
}
.rpt-drill-down-trigger.is-open {
  background: var(--brand-600);
  color: var(--brand-ink);
  border-style: solid;
  border-color: var(--brand-600);
}
.rpt-drill-down-trigger:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.rpt-drill-down-trigger svg { color: currentColor; flex-shrink: 0; }

/* Popover: anchored below the trigger, opens to the inline-start so
   the search input aligns with the trigger's leading edge. Wider than
   the trigger to comfortably hold the search input + a list of items
   without cramping. Drops above the page on small screens via the
   max-height + scroll on the list. */
.rpt-drill-down-popover {
  position: absolute;
  top: calc(100% + 6px);
  inset-inline-start: 0;
  z-index: 50;
  width: min(420px, calc(100vw - 32px));
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-lg);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: drillFade 120ms cubic-bezier(.4,0,.2,1);
}
@keyframes drillFade {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.rpt-drill-down-head {
  padding: 10px 12px 8px;
  border-bottom: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.rpt-drill-down-title {
  font-size: var(--fsz-label);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
}
[dir="rtl"] .rpt-drill-down-title { font-size: var(--fsz-body); letter-spacing: 0; text-transform: none; }
.rpt-drill-down-search-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.rpt-drill-down-search-icon {
  position: absolute;
  inset-inline-start: 9px;
  color: var(--ink-4);
  pointer-events: none;
}
.rpt-drill-down-search {
  width: 100%;
  padding: 7px 10px 7px 30px;
  font-family: inherit;
  font-size: var(--fsz-body);
  color: var(--ink);
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  outline: none;
  transition: border-color var(--t-fast), background var(--t-fast);
}
[dir="rtl"] .rpt-drill-down-search { padding: 7px 30px 7px 10px; }
.rpt-drill-down-search:focus {
  border-color: color-mix(in oklab, var(--brand-500) 45%, var(--line));
  background: var(--bg-raised);
}
.rpt-drill-down-search::placeholder { color: var(--ink-4); }

.rpt-drill-down-list {
  list-style: none;
  margin: 0;
  padding: 4px;
  max-height: 320px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.rpt-drill-down-empty {
  padding: 24px 12px;
  text-align: center;
  color: var(--ink-4);
  font-size: var(--fsz-label);
}
.rpt-drill-down-item {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  transition: background var(--t-fast);
}
.rpt-drill-down-item:hover:not(.is-disabled) { background: var(--brand-tint); }
.rpt-drill-down-item:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: -2px;
}
.rpt-drill-down-item.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.rpt-drill-down-item-main {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  flex: 1;
}
.rpt-drill-down-item-primary {
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rpt-drill-down-item-secondary {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  flex-shrink: 0;
}
.rpt-drill-down-item-secondary.mono {
  font-variant-numeric: tabular-nums;
}
.rpt-drill-down-item-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  flex-shrink: 0;
  white-space: nowrap;
}

/* ─────────── EMPTY STATE ─────────── */
.empty-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 48px 24px;
  text-align: center;
  color: var(--ink-3);
}
.empty-state-icons {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  background: var(--bg-sunken);
  border-radius: 50%;
  color: var(--ink-4);
  margin-bottom: 12px;
}
.empty-state-text {
  font-size: var(--fsz-body);
  color: var(--ink-3);
  max-width: 360px;
  line-height: 1.5;
}
[dir="rtl"] .empty-state-text { font-size: var(--fsz-body); }

/* ─────────── STEPPER (Custom Report Builder wizard) ─────────── */
.stepper { display: flex; flex-direction: column; gap: 2px; }
.step {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 12px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-family: inherit;
  font-size: inherit;
  text-align: start;
  cursor: pointer;
  color: var(--ink-3);
  transition: background var(--t-fast), color var(--t-fast);
}
.step:hover { background: var(--bg-sunken); color: var(--ink-2); }
.step-num {
  flex-shrink: 0;
  width: 24px; height: 24px;
  display: inline-grid; place-items: center;
  border-radius: 50%;
  border: 1.5px solid var(--line);
  background: var(--bg-raised);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.step-label { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.step-title {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: inherit;
  line-height: 1.3;
}
[dir="rtl"] .step-title { font-size: var(--fsz-body); }
.step-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-4);
  line-height: 1.3;
}
[dir="rtl"] .step-sub { font-size: var(--fsz-label); }
.step.is-on { color: var(--ink); background: var(--brand-tint); }
.step.is-on .step-num { background: var(--brand-600); border-color: var(--brand-600); color: var(--brand-ink); }
.step.is-on .step-title { color: var(--brand-700); }
[data-theme="dark"] .step.is-on .step-title { color: var(--brand-700); }
.step.is-done .step-num { background: var(--ok); border-color: var(--ok); color: white; }
.step.is-done .step-title { color: var(--ink-2); }

/* ─────────── REPORTS PAGE ─────────── */
.rpt-tab-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 4px;
}
.rpt-doc {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.rpt-table-wrap {
  background: var(--bg-raised);
  border: 1px solid var(--tbl-frame-border);
  border-radius: var(--tbl-frame-radius);
  /* Horizontal: auto-scroll when the table is wider than the wrap.
     The previous `overflow-x: clip` silently cut off the trailing
     columns at narrow viewports, AND let the table compress the
     first (identity) column to absorb the shrinkage — both bad for
     readability. `auto` lets the wrap scroll instead.
     Vertical: stays visible-via-clip-margin so tooltips above
     row 1 / below the last row still paint outside the wrap. */
  overflow-x: auto;
  overflow-y: visible;
  overflow-clip-margin: 32px;
  box-shadow: var(--sh-xs);
  position: relative;
}
/* Identity column rule — the first bare `<col />` in every Reports
   colgroup carries the row's identity (institute name, branch name,
   maneuver name, etc.). Previously it had no explicit width and
   absorbed all leftover space when the wrap shrank, which meant
   identity got truncated FIRST. Pinning a width forces other
   columns to share the shrinkage with horizontal scroll instead. */
.rpt-table-wrap .data-table colgroup col:first-child:not([class]):not([style]) {
  width: 240px;
}
/* Right-edge fade affordance — same vocabulary as the yard-tests
   data tables. Appears only when the wrap is overflowing
   horizontally (toggled by JS via `.is-overflowing`). Sticky pseudo
   pinned to the visible right edge, so it stays in place as the
   user scrolls. */
.rpt-table-wrap.is-overflowing::after {
  content: '';
  position: sticky;
  inset-block: 0;
  inset-inline-end: 0;
  display: block;
  block-size: 100%;
  inline-size: 36px;
  margin-inline-start: -36px;
  background: linear-gradient(to right, transparent, var(--bg-raised) 85%);
  pointer-events: none;
  z-index: 2;
  transition: opacity 160ms ease;
}
[dir="rtl"] .rpt-table-wrap.is-overflowing::after {
  background: linear-gradient(to left, transparent, var(--bg-raised) 85%);
}
.rpt-table-wrap.is-overflowing.is-scrolled-end::after { opacity: 0; }
/* Sticky-thead override. The global `.data-table thead th` rule
   uses `position: sticky; top: 40px` to dock the header below the
   compact-strip as the user scrolls the PAGE — that worked when
   `.rpt-table-wrap` was `overflow-x: clip` and the sticky context
   bubbled up to the document. With `overflow-x: auto` the wrap is
   now its own scroll container, so `top: 40px` resolves relative
   to the wrap's top edge — pushing the thead 40px DOWN onto the
   first body row. Pin it static here so the thead sits in its
   natural position above the body. Page-level sticky is lost on
   Reports tables; readable layout + working horizontal scroll
   beat that affordance. */
.rpt-table-wrap .data-table thead th { position: static; top: auto; }
/* Force a slim visible scrollbar at touch/narrow viewports so the
   horizontal scroll affordance is obvious. */
@media (max-width: 720px) {
  .rpt-table-wrap.is-overflowing {
    scrollbar-width: thin;
    scrollbar-color: color-mix(in oklab, var(--brand-500) 35%, transparent) transparent;
  }
  .rpt-table-wrap.is-overflowing::-webkit-scrollbar { block-size: 6px; }
  .rpt-table-wrap.is-overflowing::-webkit-scrollbar-thumb {
    background: color-mix(in oklab, var(--brand-500) 35%, transparent);
    border-radius: 3px;
  }
}
.rpt-table-wrap .data-table { margin: 0; }
/* Reports tables: all cells start-align per design call. `.num`
   on `<th>` and `.mono` on `<td>` keep tabular-nums so digit
   widths line up vertically inside a column even though the
   column itself is left-aligned. The chevron column (col-chev,
   below) stays end-aligned for the trailing icon. */
.rpt-table-wrap .data-table th.num,
.rpt-table-wrap .data-table td.num,
.rpt-table-wrap .data-table td.mono {
  font-variant-numeric: tabular-nums;
}
.rpt-table-wrap .data-table td.col-chev,
.rpt-table-wrap .data-table th.col-chev {
  width: 36px;
  text-align: end;
  color: var(--ink-4);
}
.rpt-table-wrap .data-table tr.is-drillable { cursor: pointer; }
.rpt-table-wrap .data-table tr.is-drillable:hover { background: var(--tbl-row-hover-bg); }
.rpt-table-wrap .data-table tr.is-drillable:hover td.col-chev { color: var(--brand-700); }
/* Visible spacer between FAIL count and PASS RATE bar — keeps the user from
   reading the fail number as part of the bar that follows. */
.rpt-table-wrap .data-table th.col-fail,
.rpt-table-wrap .data-table td.col-fail { padding-inline-end: 18px; }

/* Column-width plan for Institute Performance L1/L2 tables.
   Numeric counts (Total tests / Pass / Fail / Branches) only ever
   carry 4–7 digits, so we keep them narrow. The two bar columns
   (Pass Rate, 1st-Attempt PR) need room for the bar + percentage —
   wider, with a gap between them so the % reads against the right bar. */
.rpt-table-wrap .data-table colgroup .col-narrow { width: 108px; }
.rpt-table-wrap .data-table colgroup .col-pr     { width: 180px; }
.rpt-table-wrap .data-table colgroup .col-chev   { width: 36px; }
/* Maneuver Performance Summary lives in a split-pane (half the doc width).
   Make Tests / Passed / Failed narrower than the global `.col-narrow`
   default and drop the explicit width on the maneuver column so it can
   absorb the freed space — long names like "General Violations" then
   read end-to-end without clipping. */
.rpt-table-wrap .data-table.rpt-table-man-summary colgroup .col-maneuver-name { width: auto; }
.rpt-table-wrap .data-table.rpt-table-man-summary colgroup .col-narrow        { width: 76px; }
/* Failure Rate column trimmed twice (180→144→130px, total ~28% off the
   default `.col-pr` width) for this split-pane table; both reductions
   flow into the auto-width Maneuver column so longer names like
   "General Violations" get more breathing room without clipping. */
.rpt-table-wrap .data-table.rpt-table-man-summary colgroup .col-pr            { width: 130px; }
/* "Failure rate by branch" table reuses the man-summary structure but
   has no trailing chevron column, so the Failure Rate column sits
   flush against the table's right border. Bumping its width by 8px
   adds breathing room between the bar/value and the right edge. The
   chained class boosts specificity above the man-summary rule above
   so this override wins without `!important`. */
.rpt-table-wrap .data-table.rpt-table-man-summary.rpt-table-fail-by-branch colgroup .col-pr { width: 138px; }
.rpt-table-wrap .data-table.rpt-table-man-summary tbody td:first-child        { white-space: nowrap; }
/* Push the 1st-attempt PR column away from the headline PR so the % values
   read unambiguously against their own bar. */
.rpt-table-wrap .data-table th.col-2nd-pr,
.rpt-table-wrap .data-table td.col-2nd-pr { padding-inline-start: 18px; }

/* Schedules table — full-page-width table that mirrors the Saved
   Tests / Custom Reports responsive pattern: at wide viewports the
   table fills its container naturally (Report column absorbs the
   slack); at ≤1079 it freezes at the 1080-state width and the
   wrapper scrolls horizontally. The freeze rule is in the ≤1079
   media query block at the bottom of the file. Total fixed meta
   columns now sum to 840px (170 + 90 + 105 + 105 + 130 + 240) so
   the flex Report column sits at ~206px when the table is locked
   at 1046 — enough for "Quarterly fail-rate by branch". */

/* Examiner Performance L1 — examiners list. Drop the Branch column (L2
   profile card lists branches instead since examiners rotate). Add a
   visible inline-start padding on the Overrides column so it reads as a
   distinct group from the preceding Pass Rate bar. */
.rpt-table-wrap .data-table.rpt-table-exam-l1 colgroup .col-exam-name      { width: 180px; }
.rpt-table-wrap .data-table.rpt-table-exam-l1 thead th.col-exam-overrides .th-btn,
.rpt-table-wrap .data-table.rpt-table-exam-l1 tbody td.col-exam-overrides {
  padding-inline-start: 18px;
}

/* Institute Performance L3 — tests-in-branch grid. Student and Examiner
   are pinned to a moderate width (the marquee handles longer names with a
   slide-on-hover); the freed horizontal space goes to the metadata
   columns so they don't feel cramped. The Attempt column is widened to
   fit the new "n of m" format. */
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-tfn      { width: 122px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-date     { width: 110px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-student  { width: 168px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-veh      { width: 78px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-result   { width: 104px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-attempt  { width: 96px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-dur      { width: 100px; }
.rpt-table-wrap .data-table.rpt-table-inst-l3 colgroup .col-examiner { width: 148px; }
/* Attempt cell — the index number reads bold (the eye lands on it as the
   headline), the "of N" trailer is small + muted (just there for context). */
.cell-attempt-num {
  font-weight: 600;
  color: var(--ink);
  font-size: var(--fsz-body);
}
.cell-attempt-of {
  color: var(--ink-3);
  font-weight: 400;
  font-size: var(--fsz-caption);
  margin-inline-start: 3px;
}
[dir="rtl"] .cell-attempt-of { font-size: var(--fsz-label); }

/* Test Analysis parameter tables — give the parameter column most of the
   width because parameter names can be long full sentences ("Touching the
   exit line (partially or pass it completely) without completing maneuver").
   The numeric / severity columns only need enough room for their content. */
/* .rpt-table-wrap prefix dropped from these rules so the dashboard
   TopTenTable can reuse the same params-table chrome without being
   wrapped in the report-style frame. The `.rpt-table-params`
   compound class is unique enough to scope safely on its own. */
.data-table.rpt-table-params colgroup .col-rank       { width: 36px; }
.data-table.rpt-table-params colgroup .col-sev        { width: 100px; }
.data-table.rpt-table-params colgroup .col-pct-narrow { width: 76px; }
.data-table.rpt-table-params { table-layout: fixed; }
.data-table.rpt-table-params td:first-child,
.data-table.rpt-table-params td:nth-child(2) {
  /* The parameter cell uses .cell-marquee for ellipsis + slide-on-hover. */
  overflow: hidden;
}
.data-table.rpt-table-params thead th:last-child .th-btn,
.data-table.rpt-table-params tbody td:last-child {
  padding-inline-end: 14px;
}

/* Cell-marquee — host wrapper around a single .marquee-inner span.
   On hover the inner span gets a `transform: translateX(...)` animation
   (GPU-composited, no per-frame layout) so long text smoothly slides to
   reveal its end. The host has overflow:hidden + a fade-gradient mask on
   the trailing edge as a "there's more" cue (replaces text-overflow:
   ellipsis, which is incompatible with translating an inline-block child
   past the container edge). */
.cell-marquee {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  max-width: 100%;
  cursor: default;
  /* Host needs to be the positioning context for the inner span. */
  position: relative;
}
.cell-marquee .marquee-inner {
  display: inline-block;
  white-space: nowrap;
  /* Default state: no transform. Animation sets transform via Web
     Animations API; we keep transition off here so the explicit duration
     wins. `will-change: transform` keeps the browser composited path warm
     when the user is about to hover. */
  will-change: transform;
}
/* Fade-gradient mask — applied ONLY when the host is actually too narrow
   for its inner text. The `is-overflowing` class is toggled by a JS scan
   that compares the inner span's scrollWidth to the host's clientWidth.
   Doing it this way avoids fading the last few characters of short names
   that happen to sit inside a short column. */
.cell-marquee.is-overflowing {
  -webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%);
          mask-image: linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%);
}
[dir="rtl"] .cell-marquee.is-overflowing {
  -webkit-mask-image: linear-gradient(to left, black 0, black calc(100% - 14px), transparent 100%);
          mask-image: linear-gradient(to left, black 0, black calc(100% - 14px), transparent 100%);
}

/* Numbers in Reports tables align with text (start), not flush-right.
   Tables here are read top-to-bottom to compare entries, not column-totalled,
   so left/start alignment groups the value next to its row label. */
.rpt-table-wrap .data-table .th-btn { justify-content: flex-start; }
.rpt-table-wrap .data-table .th-btn[style*="justify-content: center"] { justify-content: center !important; }
/* Header icon slot — replaces the semantic icon with a sort indicator on
   hover or when this column is the active sort key. All three children
   (default icon, hover hint, active arrow) share the same slot via
   absolute positioning, so swapping never shifts the label horizontally. */
.th-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}
.th-icon > svg:first-child { width: 14px; height: 14px; }
.th-icon > svg { transition: opacity var(--t-fast); }
.th-icon-hint,
.th-icon-active {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}
/* Defaults: semantic icon visible, hover/active SVGs hidden. */
.th-icon-hint   { opacity: 0; }
.th-icon-active { opacity: 0; }
/* Hover (no active sort yet) — swap the semantic icon out for the hint
   stack at the SAME position. */
.th-btn:hover:not(.is-sorted) .th-icon > svg:first-child { opacity: 0; }
.th-btn:hover:not(.is-sorted) .th-icon-hint              { opacity: 1; }
/* Sorted column — semantic icon and hint are off; the active arrow takes
   the slot. */
.th-btn.is-sorted .th-icon > svg:first-child { opacity: 0; }
.th-btn.is-sorted .th-icon-hint              { opacity: 0; }
.th-btn.is-sorted .th-icon-active            { opacity: 1; }

/* Total row sits right under the last data row. Reads as a summary
   via the top-border + padding + cell-strong weight on the leading
   cell — does NOT need a smaller font (which previously dropped to
   12px / label and looked dropped a tier below body rows). Matches
   body-row size now so columns line up cleanly. */
.rpt-table-wrap .data-table tr.rpt-totals td {
  padding-top: 12px;
  padding-bottom: 12px;
  font-size: var(--fsz-body);
}
/* Hint above tables that contain drillable rows */
.rpt-drill-hint {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  padding-inline-start: 4px;
  margin-top: -2px;
}
[dir="rtl"] .rpt-drill-hint { font-size: var(--fsz-label); }
.rpt-drill-hint svg { color: var(--brand-700); }
.rpt-table-wrap .data-table tr.rpt-totals {
  font-weight: 600;
  background: var(--bg-sunken);
}
.rpt-table-wrap .data-table tr.rpt-totals td { border-top: 1px solid var(--line-strong); }
/* The "TOTAL" label cell in any totals row mirrors the table
   header chrome — uppercase, tracked, caption-tier 11/600 ink-3.
   Reads as "this is a summary band that closes the table" rather
   than "another data row whose name happens to be Total". */
.rpt-table-wrap .data-table tr.rpt-totals td:first-child,
.rpt-table-wrap .data-table tr.rpt-totals td:first-child.cell-strong {
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.rpt-table-wrap .data-table .cell-strong { font-weight: 600; color: var(--ink); }
.rpt-table-wrap .data-table .cell-sub { font-size: var(--fsz-caption); color: var(--ink-3); margin-top: 1px; }
.rpt-table-wrap .data-table .cell-ok  { color: var(--ok); }
.rpt-table-wrap .data-table .cell-err { color: var(--err); }

/* Two-column report layout (e.g. Maneuver L1: summary + Top 10) */
.rpt-split {
  display: grid;
  /* Equal-width side-by-side panes at ≥1080 — the previous 1.1:1
     weighting biased the left pane (Maneuver Performance Summary at
     L1, "Failed parameters in <maneuver>" at L2) to make room for
     longer maneuver names, but left the two cards visually
     misaligned. Equal columns keep the row balanced and the page
     reads as a clean grid. The ≤1079 query collapses this to a
     single column anyway, so the equal-fr split applies only at
     wide viewports. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 14px;
}
.rpt-split-pane { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.rpt-split-head {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.01em;
  padding-inline-start: 2px;
}
[dir="rtl"] .rpt-split-head { font-size: var(--fsz-body); letter-spacing: 0; }

/* Section header (used by Schedules, Templates, Builder header rows) */
.rpt-section-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.rpt-section-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  line-height: 1.2;
}
[dir="rtl"] .rpt-section-title { letter-spacing: 0; }
.rpt-section-sub {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  margin-top: 4px;
  line-height: 1.4;
}
[dir="rtl"] .rpt-section-sub { font-size: var(--fsz-body); }
.rpt-section-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; flex-wrap: wrap; }

/* Maneuver cards (Institute L4 — single test detail) */
.maneuver-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px;
}
.maneuver-card {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
  display: flex; flex-direction: column; gap: 8px;
  box-shadow: var(--sh-xs);
}
.maneuver-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px;
}
.maneuver-card-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
}
.maneuver-card-meta {
  display: flex; align-items: center; gap: 12px;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
}
[dir="rtl"] .maneuver-card-meta { font-size: var(--fsz-label); }
.maneuver-card-meta strong {
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.maneuver-card-top {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  border-top: 1px solid var(--line);
  padding-top: 6px;
  line-height: 1.4;
}
[dir="rtl"] .maneuver-card-top { font-size: var(--fsz-label); }

/* Inline icon-button row (used in Schedules + Templates lists) */
.row-icon-btns { display: inline-flex; align-items: center; gap: 2px; }
.row-icon-btn {
  /* sm icon-only tier — compact for tables. See tokens.css `--cta-h-*`. */
  width: var(--cta-h-sm); height: var(--cta-h-sm);
  display: inline-grid; place-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-3);
  cursor: pointer;
  padding: 0;
  transition: background var(--t-fast), color var(--t-fast);
}
.row-icon-btn:hover { background: var(--brand-tint); color: var(--brand-700); }

/* Templates table — inherits the standard data-table styling. The
   row height is bumped slightly (56px vs 60px default) because
   the cells stack two lines (name + description). Scroll +
   freeze behavior comes from the global ≤1079 rule — same as
   every other table. */
.tpl-table-card .data-table tbody td,
.tpl-table-card .data-table tbody tr { height: 56px; }
.tpl-name-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.tpl-name-cell .tpl-name-text {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[dir="rtl"] .tpl-name-cell .tpl-name-text { font-size: var(--fsz-body); }
.tpl-name-cell .tpl-desc {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[dir="rtl"] .tpl-name-cell .tpl-desc { font-size: var(--fsz-label); }

/* Visibility pill — Private (lock icon, neutral ink) vs Public (globe
   icon, brand tint). Same pill grammar; the icon + colour combo
   carries the semantic. */
/* `.tpl-shared` composes the unified `.pill` primitive — see top of
   file. Tone (brand / info / neutral) is set via `.is-{tone}` next
   to `.tpl-shared`. No callsite-specific overrides currently needed;
   the class stays on the element only as a hook for future tweaks
   and to make it greppable as "the visibility chip on the templates
   / schedules table". */

/* Lifecycle status pill — three semantic colors so the user can
   scan the column at a glance:
     • active   — ok-green (running + scheduled)
     • inactive — neutral ink (paused but saved)
     • draft    — warn-amber (not yet shipped, editable)
   The colored dot + text-tint is enough; no border to keep the row
   line scannable. */
.tpl-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  padding: 2px 9px;
  border-radius: var(--r-pill);
}
/* Removed: was bumping pill font-size in RTL, breaking the
   11px caption-tier consistency that all sibling pills share. */
.tpl-status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
  box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 12%, transparent);
}
.tpl-status--active   { color: var(--ok); background: color-mix(in oklab, var(--ok) 10%, var(--bg-raised)); }
.tpl-status--active   .tpl-status-dot { background: var(--ok); }
.tpl-status--inactive { color: var(--ink-3); background: var(--bg-sunken); }
.tpl-status--inactive .tpl-status-dot { background: var(--ink-3); }
.tpl-status--draft    { color: var(--warn, oklch(0.62 0.16 75)); background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 10%, var(--bg-raised)); }
.tpl-status--draft    .tpl-status-dot { background: var(--warn, oklch(0.72 0.15 75)); }

/* Disabled action button — keeps icon visible but greys it out + no
   pointer events. Tooltip on the same element explains why (e.g.
   "Deactivate to edit"). */
.row-icon-btn.is-disabled,
.row-icon-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.row-icon-btn.is-disabled:hover,
.row-icon-btn:disabled:hover { background: transparent; color: var(--ink-3); }

/* Custom-Reports filter bar — search input on the left, multi-
   select pills + date-range pill + Clear on the right. Reuses the
   `.filter-bar--reports` row layout so it visually matches the
   filter bars on the other report tabs. */
.tpl-filter-bar { margin-bottom: 8px; }
/* Search width trimmed 10% (320 → 288) so the pill row gets a bit
   more breathing room on the same flex line. The ≤1279 query trims
   another step (260 ≈ 280 × 10%) for narrower viewports. */
.tpl-filter-search { flex: 0 0 288px; }
/* Pills stay on a single row inside the Custom Reports filter bar.
   The chained-class selector (`.filter-bar--reports.tpl-filter-bar`)
   bumps specificity to 3 classes so this `nowrap` declaration wins
   over `.filter-bar--reports .filter-pills { flex-wrap: wrap }`
   declared later in the file (same-specificity cascade would
   otherwise pick the later rule, which is what was making the pills
   break onto a 2nd row in the 900–905 zone). At ≤899 the bar itself
   wraps so the pills sit on their own row beneath the search; nowrap
   on the pills container is still correct there since the row is
   already wide enough to hold all pills inline. */
.filter-bar--reports.tpl-filter-bar .filter-pills { flex-wrap: nowrap; }
/* On mobile the nowrap rule above causes the pill row to overflow
   the filter bar (Visibility / Status / Last run / Owner can't
   all fit at 375px). Switch to wrap so each pill stays fully
   visible; the bar gets a bit taller but the user can see all
   filters at once. */
@media (max-width: 720px) {
  .filter-bar--reports.tpl-filter-bar .filter-pills { flex-wrap: wrap; }
}
@media (max-width: 1279px) {
  /* Narrow viewports: shrink the search input so the pills + Clear
     fit beside it on one line. 252 ≈ 10% off the previous 280
     baseline; still leaves room for typing template names. */
  .tpl-filter-bar .filter-search { flex: 0 1 252px; }
}

/* Custom Reports templates table —
   At ≥1080 the table fills its container with no horizontal scroll;
   the flex Template column absorbs all remaining space (and shrinks
   along with the viewport since the meta columns are fixed-width:
   110 + 90 + 120 + 95 + 70 + 128 + 240 = 853px). At ≤1079 the global
   `.table-scroll` rule in the breakpoint query freezes the table at
   1046px min-width and enables horizontal scroll, so Template stops
   shrinking the moment the freeze kicks in. */
/* Empty-row inside the templates table — used when filters narrow
   the list to zero matches. Spans all columns and centers the copy
   so the table stays visually balanced rather than collapsing. */
.tpl-empty-row {
  text-align: center;
  padding: 32px 16px !important;
  color: var(--ink-3);
  font-size: var(--fsz-body);
}

/* Visibility cell — pill stacked above a small "shared with" caption
   so the cell carries who-can-see-this context without needing a
   separate column. */
.tpl-vis-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  min-width: 0;
}

/* Last-run cell — date on top (regular ink), time below in mono +
   muted ink. Two-line stack lets the column stay narrow while
   surfacing precise timing. */
.tpl-when-cell {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
/* Date/time stack — primary date line at body size (14px) so it
   reads at the same baseline as every other primary content cell
   in the row (Owner, Last run by, Run button), with the time
   subtitle dropping to caption (11px) ink-3 below it. The
   `.data-table .mono` (0,0,2,0) override on `.tpl-when-time`
   would otherwise re-bump the time to 14px, which is why both
   selectors are duplicated at matched specificity. */
.tpl-when-date,
.data-table .tpl-when-date {
  font-size: var(--fsz-body);
  color: var(--ink);
  line-height: 1.3;
}
.tpl-when-time,
.data-table .tpl-when-time {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}

/* Last-run-by cell — icon + name. Body size (14px) so it shares
   the baseline with Owner / Run / Last-run-date in the same row.
   System runs get a cog glyph in brand-ink; manual runs get a
   person silhouette in muted ink. The icon's hue is the primary
   "automated vs human" cue. */
.tpl-by-cell {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-body);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.tpl-by-cell svg { color: var(--ink-3); flex-shrink: 0; }
.tpl-by-cell.is-system {
  color: var(--brand-700);
}
.tpl-by-cell.is-system svg { color: var(--brand-600); }
.tpl-shared-with {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
[dir="rtl"] .tpl-shared-with { font-size: var(--fsz-caption); }

.tpl-actions-cell,
.sched-actions-cell {
  /* Block-level flex (not inline) + width:100% + justify-content
     flex-end keeps the action cluster pinned to the row's end even
     when buttons are conditionally hidden (e.g. Run is suppressed
     for draft templates). With inline-flex the cluster shrunk to
     content width and drifted left because `text-align: end` on
     the parent td didn't reliably re-position it. Now the cluster
     always anchors to the right border, so draft rows visually
     align with active rows above and below. */
  display: flex;
  width: 100%;
  justify-content: flex-end;
  align-items: center;
  gap: 4px;
  /* Matches the system-standard cell padding so the visual gap
     between this cell and the previous one is the same as every
     other column transition. End-padding 16px gives the rightmost
     icon button visible breathing room from the table's right
     border (the column is 240/260px wide to fit Run + 4 icons +
     this padding without crushing). Shared between Custom Reports
     and Schedules so both action clusters read identically. */
  padding-inline-start: 6px;
  padding-inline-end: 16px;
}
.tpl-actions-cell .btn,
.sched-actions-cell .btn { margin-inline-end: 4px; }
.tpl-actions-cell .row-icon-btn,
.sched-actions-cell .row-icon-btn {
  /* Always-visible (not just on row-hover) since this is the only
     way to access these actions in the table rows. */
  opacity: 1;
}
.tpl-actions-cell .row-icon-btn.is-disabled,
.tpl-actions-cell .row-icon-btn:disabled,
.sched-actions-cell .row-icon-btn.is-disabled,
.sched-actions-cell .row-icon-btn:disabled {
  opacity: 0.4;
}
/* CRITICAL: data-tip tooltips on the action buttons (Edit / Duplicate
   / Schedule) anchor above the button via a ::after pseudo-element.
   The .data-table td has `overflow: hidden` by default (so other
   columns can ellipsize long text), which clips the tooltip at the
   cell's top edge. Same fix as `.cell-vehicle` and `.cell-result`:
   opt this cell out of the table-wide overflow rule. The tooltip
   also needs a higher z-index than the sticky thead (z:10). */
.data-table td.tpl-actions-cell,
.data-table td.sched-actions-cell { overflow: visible; }

/* ─────────── UNIFIED IN-TABLE TOOLTIP RULE ───────────
   Every tooltip rendered inside a `.data-table tbody` (action
   buttons, vehicle tags, result pills, etc.) anchors BELOW the
   trigger and sits at z-index 200. Why:
     • The sticky thead (`position: sticky; z-index: 10`) creates
       its own stacking context. Tooltips rendered ABOVE the trigger
       on row 1 were being clipped or covered by the header even
       with z-index workarounds. Flipping to below sidesteps the
       conflict — the tip paints into the row-line gap below the
       row, never into the header's vertical territory.
     • Sharing one rule means every per-feature class (vehicle-tag,
       result-flat, row-icon-btn in tpl/sched action cells, …)
       behaves identically without each one redefining z-index +
       offset overrides that drift apart over time. */
.data-table tbody [data-tip]::after {
  bottom: auto;
  top: calc(100% + 8px);
  z-index: 200;
}

/* Builder layout */
.builder {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  gap: 14px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 14px;
  box-shadow: var(--sh-xs);
}
/* On mobile/narrow the 240px+1fr grid forces the right column to
   ~120px on a 375px viewport, which makes the right pane content
   wrap to single-word lines AND the source cards overflow off
   the right edge. Stack the columns vertically below the
   breakpoint — stepper on top, panel below. */
@media (max-width: 720px) {
  .builder {
    grid-template-columns: 1fr;
    gap: 12px;
    padding: 12px;
  }
  .builder-side {
    border-inline-end: 0;
    padding-inline-end: 0;
    /* On mobile the stepper sits ABOVE the content and a bottom
       hairline replaces the right-edge border. */
    border-block-end: 1px solid var(--line);
    padding-block-end: 12px;
  }
  .builder-main {
    padding: 0;
  }
}
.builder-side {
  border-inline-end: 1px solid var(--line);
  padding-inline-end: 12px;
}
.builder-main {
  padding: 6px 4px 0 12px;
  display: flex;
  flex-direction: column;
  /* Reserve room for the wizard footer at the bottom of the panel.
     min-height: 0 so the inner content area can shrink and scroll
     instead of pushing the footer below the viewport. */
  min-height: 0;
}
[dir="rtl"] .builder-main { padding: 6px 12px 0 4px; }
.builder-main-content {
  flex: 1 1 auto;
  min-height: 0;
}
/* Wizard nav footer — Cancel pinned to the start, Back/Next/Save
   group on the end. Sticky bottom-aligned via flex above so it stays
   visible whether the step content is short or long-scrolling. */
.builder-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  margin-top: 16px;
  border-top: 1px solid var(--line);
  /* On narrow viewports the right-side group (Back + Save-as-draft +
     Save template at Step 7) overflows the builder card. Allow the
     footer to wrap so Cancel stays left and the right group drops
     onto its own row instead of clipping past the edge. */
  flex-wrap: wrap;
  row-gap: 8px;
}
.builder-foot-end {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Wrap inside the right-side group so Back / Save-as-draft / Save
     template can stack at very narrow widths without breaking the
     wizard footer's containment. */
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: 8px;
}

.builder-step-head { margin-bottom: 14px; }
.builder-step-title {
  margin: 0;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
}
[dir="rtl"] .builder-step-title { letter-spacing: 0; }
.builder-step-sub {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  margin-top: 4px;
  line-height: 1.5;
  max-width: 640px;
}
[dir="rtl"] .builder-step-sub { font-size: var(--fsz-body); }
.builder-step-stub { display: flex; flex-direction: column; gap: 12px; }

/* Step 1 — data sources grid */
.source-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 8px;
}
.source-card {
  display: flex; align-items: center; gap: 12px;
  padding: 12px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  text-align: start;
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  position: relative;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.source-card:hover { border-color: var(--line-strong); background: color-mix(in srgb, var(--brand-500) 2%, var(--bg-raised)); }
.source-card.is-on {
  border-color: color-mix(in oklch, var(--brand-500) 40%, var(--line));
  background: var(--brand-tint);
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--brand-500) 25%, transparent);
}
.source-card-icon {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  background: var(--bg-sunken);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  flex-shrink: 0;
}
.source-card.is-on .source-card-icon { background: var(--brand-tint-2); color: var(--brand-700); }
.source-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.source-card-name { font-size: var(--fsz-body); font-weight: 600; color: var(--ink); }
.source-card-desc { font-size: var(--fsz-caption); color: var(--ink-3); }
[dir="rtl"] .source-card-desc { font-size: var(--fsz-label); }
.source-card-check {
  position: absolute;
  top: 8px;
  inset-inline-end: 8px;
  width: 18px; height: 18px;
  display: grid; place-items: center;
  background: var(--brand-600);
  color: var(--brand-ink);
  border-radius: 50%;
}

/* Step 2 — fields-checkbox grid. Min-card 220 lets us hit 3
   columns at builder-main width ≈740 (which is what we get at
   1079 viewport: 1047 main inner − 240 stepper − 14 gap −
   builder padding/main padding ≈ 740). At 1280+ viewport the
   main grows enough for 4 columns. With 8 groups total that
   gives a 2-row × 4-col arrangement on desktop or 3-row × 3-col
   at the 1079 freeze. */
.fields-grid {
  display: grid;
  /* Lock to exactly 3 columns at ≥1080 — at wider viewports the grid
     would otherwise fit a 4th column and disrupt the visual rhythm of
     the 8 field groups. The ≤1079 media query drops to 2 columns. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  align-items: start;
}
.fields-group {
  display: flex; flex-direction: column; gap: 4px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 10px 12px;
  /* Cap each group's height so the grid stays balanced — long
     groups (20+ fields) scroll internally instead of stretching the
     whole row. The list inside has its own overflow:auto. */
  max-height: 280px;
}
.fields-group-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow-y: auto;
  /* Reserve scrollbar space so the rightmost checkbox label doesn't
     shift when scroll kicks in. */
  scrollbar-gutter: stable;
  /* Pad inline-end so the scrollbar (when shown) doesn't visually
     hug the labels. */
  padding-inline-end: 2px;
}
.fields-group-name {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
  margin-bottom: 4px;
}
[dir="rtl"] .fields-group-name { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.fields-check {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 4px;
  font-size: var(--fsz-label);
  color: var(--ink);
  cursor: pointer;
  border-radius: var(--r-sm);
}
[dir="rtl"] .fields-check { font-size: var(--fsz-body); }
.fields-check:hover { background: var(--bg-sunken); }
.fields-check input[type="checkbox"] {
  width: 14px; height: 14px;
  accent-color: var(--brand-600);
  cursor: pointer;
}

/* Step 2 — toolbar (search + global Clear) above the field grid.
   The search re-uses the global `.filter-search` styling so it
   matches the look of every other search input in the app. */
.fields-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
.fields-search { flex: 1 1 320px; }
.fields-toolbar .filter-reset { flex-shrink: 0; }
.fields-empty {
  grid-column: 1 / -1;
  padding: 32px 16px;
  text-align: center;
  color: var(--ink-3);
  font-size: var(--fsz-body);
  background: var(--bg-raised);
  border: 1px dashed var(--line);
  border-radius: var(--r-md);
}

/* Per-group header with name + Select all / Clear toggle. */
.fields-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 4px;
}
.fields-group-head .fields-group-name { margin-bottom: 0; }
.fields-group-toggle {
  font-family: inherit;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  background: transparent;
  border: 0;
  padding: 2px 4px;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in oklab, var(--ink-3) 50%, transparent);
  transition: color var(--t-fast), text-decoration-color var(--t-fast);
}
[dir="rtl"] .fields-group-toggle { font-size: var(--fsz-label); }
.fields-group-toggle:hover { color: var(--brand-700); text-decoration-color: var(--brand-700); }
.fields-group-toggle.is-all { color: var(--brand-700); text-decoration-color: var(--brand-700); }

/* Step 3 — selected-values chips below each filter dropdown. Lets
   the user see all current selections at a glance without expanding
   the pill, plus remove individual values with the × on each chip
   and a global Clear at the end of the row. */
.builder-filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
  align-items: center;
}
.builder-filter-empty {
  margin-top: 4px;
  font-size: var(--fsz-caption);
  color: var(--ink-4);
  font-style: italic;
}
[dir="rtl"] .builder-filter-empty { font-size: var(--fsz-label); }
.builder-filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 4px 2px 10px;
  background: var(--brand-tint);
  border: 1px solid color-mix(in oklch, var(--brand-500) 25%, var(--line));
  border-radius: var(--r-pill);
  font-size: var(--fsz-label);
  color: var(--brand-700);
}
[dir="rtl"] .builder-filter-chip { padding: 2px 10px 2px 4px; font-size: var(--fsz-label); }
.builder-filter-chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 0;
  background: transparent;
  color: var(--brand-700);
  cursor: pointer;
  padding: 0;
  transition: background var(--t-fast);
}
.builder-filter-chip-x:hover { background: color-mix(in oklab, var(--brand-500) 20%, transparent); }
.builder-filter-clear {
  margin-inline-start: 4px;
  /* Inherits the standard `.filter-reset` underline-text style. */
}

/* Step 5 — limit-mode toggle + conditional number input. */
.builder-limit-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* Step 7 — schedule row. Schedule preset, optional day-picker, and
   the Run-at time picker share one flex row so related controls
   stay grouped. Each field uses a `flex: 1 1 180px` basis so they
   wrap to a stack on narrow viewports while filling the row evenly
   on wider ones. The schedule preset gets a slightly larger basis
   since its options are longer ("Bi-weekly on a specific day"). */
.builder-sched-row {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
}
.builder-sched-row .builder-field {
  flex: 1 1 180px;
  min-width: 0;
}
.builder-sched-row .builder-sched-primary { flex: 1 1 240px; }
.builder-sched-row .builder-sched-day     { flex: 0 1 160px; }
.builder-sched-row .builder-sched-time    { flex: 0 1 240px; min-width: 0; }

/* Step 7 — custom schedule mini-form. Groups date+time on one row,
   repeat on its own row below, inside a subtly-tinted container so
   it visually nests under the schedule preset that opened it. */
.builder-custom-sched {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

/* Steps 3–7 — shared form layout. Stacked vertically with consistent
   field labels, controls, and hint text. `.builder-row` lets two
   fields sit side-by-side on wide viewports; collapses to stacked
   below 720px so labels stay legible. */
.builder-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 720px;
}
.builder-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* Segmented toggles inside a builder-field hug their content width
   (Ascending / Descending sized to text + padding) instead of being
   stretched to the field's full column width by the flex parent's
   default `align-items: stretch`. */
.builder-field > .seg-toggle { align-self: flex-start; }
.builder-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}
@media (max-width: 720px) {
  .builder-row { grid-template-columns: 1fr; }
}
.builder-field-label {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.02em;
}
[dir="rtl"] .builder-field-label { font-size: var(--fsz-label); letter-spacing: 0; }
.builder-field-req { color: var(--err); margin-inline-start: 2px; }
.builder-field-hint {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  line-height: 1.4;
}
[dir="rtl"] .builder-field-hint { font-size: var(--fsz-label); }
.builder-checks {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Form controls — match the rest of the design system. Native
   <select> styling with a chevron pseudo, native <input> for text +
   number, native <textarea>. */
.builder-input,
.builder-textarea,
.builder-select {
  width: 100%;
  font-family: inherit;
  font-size: var(--fsz-body);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 9px 12px;
  outline: 0;
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.builder-input:focus,
.builder-textarea:focus,
.builder-select:focus {
  border-color: color-mix(in oklab, var(--brand-500) 45%, var(--line));
  box-shadow: 0 0 0 3px var(--ring);
}
.builder-input--narrow { width: 140px; }
.builder-textarea { resize: vertical; min-height: 60px; line-height: 1.4; }
.builder-select {
  appearance: none;
  -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
                    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
  background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-inline-end: 32px;
  cursor: pointer;
}
[dir="rtl"] .builder-select {
  background-position: 13px 50%, 18px 50%;
  background-image: linear-gradient(135deg, transparent 50%, var(--ink-3) 50%),
                    linear-gradient(45deg, var(--ink-3) 50%, transparent 50%);
  padding-inline-end: 12px;
  padding-inline-start: 32px;
}
.builder-select:disabled { color: var(--ink-4); cursor: not-allowed; }

/* SingleSelect / TimePicker / SingleDatePicker — custom dropdown
   trigger that looks like a styled input but opens the global
   `.filter-menu` popover on click. Replaces native <select> in the
   builder so every dropdown shares the same look + open state +
   highlighted item grammar as the filter pills elsewhere in the app. */
.builder-select-wrap { position: relative; display: inline-flex; }
.builder-select-wrap.is-full { display: flex; width: 100%; }
.builder-select-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: auto;
  min-width: 100px;
  font-family: inherit;
  font-size: var(--fsz-body);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 9px 12px;
  cursor: pointer;
  text-align: start;
  white-space: nowrap;
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.builder-select-trigger.is-full { width: 100%; }
.builder-select-trigger.is-narrow { min-width: 80px; padding: 9px 10px; }
.builder-select-trigger:hover { border-color: var(--line-strong); }
.builder-select-trigger:focus-visible,
.builder-select-trigger.is-open {
  outline: 0;
  border-color: color-mix(in oklab, var(--brand-500) 45%, var(--line));
  box-shadow: 0 0 0 3px var(--ring);
}
.builder-select-trigger > svg { color: var(--ink-3); flex-shrink: 0; }
.builder-select-trigger.is-open > svg:last-child { transform: rotate(180deg); }
.builder-select-trigger-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* TimePicker — three SingleSelects laid out horizontally with a
   colon separator between hour and minute. */
.builder-time-picker {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
}
.builder-time-sep {
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink-3);
  user-select: none;
  line-height: 1;
}
/* Compact the three SingleSelects inside a TimePicker so HH/MM/AM-PM
   fit inside the 240px `.builder-sched-time` field without overflowing
   the container border. The default `.builder-select-trigger` min-width
   of 100px would push three selects + separators well past 240px. */
.builder-time-picker .builder-select-trigger {
  min-width: 64px;
  padding: 9px 8px;
}

/* SingleDatePicker popover — calendar-only (no presets list). The
   month grid is the same `CalendarMonth` used by DateRangeFilter so
   the look matches the filter calendar pixel-for-pixel. */
.single-date-menu {
  padding: 0;
}
.single-date-menu .cal-month {
  /* Slightly tighter padding than the range version since there's
     just one month, not two side-by-side. */
  padding: 8px;
}

/* No-results row inside the SingleSelect menu (shown when the search
   query has no matches). */
.filter-menu-empty {
  padding: 12px;
  text-align: center;
  color: var(--ink-4);
  font-size: var(--fsz-label);
}
.builder-input::placeholder,
.builder-textarea::placeholder { color: var(--ink-4); }

/* .builder-seg — retired. Use .seg-toggle.is-compact (inline-flex
   content-sized; no .is-stretch for builder fields). */

/* Step 7 — visibility radio group. Each option is a card with icon +
   name + hint. Selected card gets brand-tint + primary border. Same
   grammar as the GenerateReportModal's format picker so the wizard's
   pattern stays familiar. */
.builder-radio-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Visibility segmented control — used by Builder Step 7 + the
   Create-schedule modal. Three pills on a single row (icon + name)
   instead of stacked radio cards. The active pill's hint shows
   beneath in muted ink so the explanatory copy still has a place. */
.vis-seg {
  display: inline-flex;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 2px;
  gap: 2px;
  align-self: flex-start;
  flex-wrap: wrap;
}
.vis-seg-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
  white-space: nowrap;
}
[dir="rtl"] .vis-seg-opt { font-size: var(--fsz-body); }
.vis-seg-opt:hover { color: var(--ink); background: color-mix(in oklab, var(--ink) 4%, transparent); }
.vis-seg-opt.is-on {
  background: var(--bg-raised);
  color: var(--brand-700);
  box-shadow: 0 1px 2px -1px rgba(0,0,0,0.08);
}
.vis-seg-opt > svg { color: currentColor; flex-shrink: 0; }
.vis-seg-opt:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}
.vis-seg-hint {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  margin-top: 4px;
  line-height: 1.35;
}
[dir="rtl"] .vis-seg-hint { font-size: var(--fsz-label); }
@media (max-width: 480px) {
  /* Pills stack to two rows on tiny phones — the segmented row
     would otherwise overflow horizontally past the modal edge. */
  .vis-seg { display: flex; }
  .vis-seg-opt { flex: 1 1 0; justify-content: center; }
}
.builder-radio-opt {
  display: grid;
  /* col 1 = radio dot, col 2 = icon, col 3 = name+hint stack */
  grid-template-columns: auto auto 1fr;
  column-gap: 10px;
  align-items: center;
  padding: 10px 12px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.builder-radio-opt:hover { border-color: var(--line-strong); }
.builder-radio-opt.is-on {
  background: var(--brand-tint);
  border-color: color-mix(in oklch, var(--brand-500) 35%, var(--line));
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--brand-500) 20%, transparent);
}
.builder-radio-opt input[type="radio"] {
  grid-column: 1;
  grid-row: 1 / span 2;
  accent-color: var(--brand-600);
  cursor: pointer;
  align-self: center;
}
.builder-radio-opt > svg {
  grid-column: 2;
  grid-row: 1 / span 2;
  color: var(--ink-3);
  align-self: center;
}
.builder-radio-opt.is-on > svg { color: var(--brand-700); }
.builder-radio-name {
  grid-column: 3;
  grid-row: 1;
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
}
.builder-radio-hint {
  grid-column: 3;
  grid-row: 2;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  line-height: 1.35;
}
[dir="rtl"] .builder-radio-hint { font-size: var(--fsz-label); }

/* Step 6 — config summary. Stacked label/value rows mirror the
   GenerateReportModal's confirmation block so the wizard's review
   surface uses the same grammar. */
.builder-summary-label {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  margin-bottom: 12px;
}
[dir="rtl"] .builder-summary-label { font-size: var(--fsz-body); }
.builder-summary {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
}
.builder-summary-row {
  display: grid;
  /* The label column reads as a "row header" — fixed-width and
     visually distinct from the value column via background +
     divider. Mirrors the data-table thead grammar (sunken bg,
     uppercase + small + muted typography) so the summary feels
     like a transposed report header. */
  grid-template-columns: 200px 1fr;
  align-items: stretch;
  border-bottom: 1px solid var(--line);
}
.builder-summary-row:last-child { border-bottom: 0; }
.builder-summary-row dt {
  background: var(--bg-sunken);
  border-inline-end: 1px solid var(--line);
  padding: 12px 14px;
  margin: 0;
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-3);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
}
[dir="rtl"] .builder-summary-row dt { font-size: var(--fsz-caption); letter-spacing: 0; text-transform: none; }
.builder-summary-row dd {
  font-size: var(--fsz-body);
  color: var(--ink);
  margin: 0;
  min-width: 0;
  padding: 12px 14px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}
[dir="rtl"] .builder-summary-row dd { font-size: var(--fsz-body); }
.builder-summary-empty {
  color: var(--ink-4);
  font-style: italic;
}
.builder-summary-chips {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
}
.builder-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  white-space: nowrap;
}
[dir="rtl"] .builder-chip { font-size: var(--fsz-label); }
.builder-summary-cta {
  display: flex;
  justify-content: flex-end;
  margin-top: 16px;
}
/* Inline preview generator. Uses the same gen-progress-bar tokens
   as the export modal so the visual language is consistent. */
.builder-preview-loader {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 32px 24px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
/* Preview header — caption on the left, regenerate button on the
   right. Sits above the table. */
.builder-preview-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}
.builder-preview-meta {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  letter-spacing: 0.02em;
}
[dir="rtl"] .builder-preview-meta { font-size: var(--fsz-label); }

.builder-step7-hint { margin-top: 4px; }
/* Allow both axes to scroll inside the preview card. The default
   .table-card has `overflow-x: clip` (so other tables can ellipsize
   long cells) but the preview wants horizontal scroll when the user
   selects many fields — they'd rather scroll than have each column
   crushed to ~30px. */
.builder-preview-card { overflow: hidden; }
.builder-preview-card .table-scroll {
  max-height: 380px;
  overflow: auto;
}
/* Switch to `auto` table-layout so each column sizes to its content
   instead of the default `fixed` 1/N split. Combined with `width:
   max-content` and `min-width: 100%`, the table grows beyond the
   container and triggers horizontal scroll only when needed. Each
   cell gets a sensible min-width so headers never get crushed. */
.builder-preview-card .data-table {
  table-layout: auto;
  width: max-content;
  min-width: 100%;
}
.builder-preview-card .data-table th,
.builder-preview-card .data-table td {
  min-width: 130px;
  white-space: nowrap;
}
.builder-preview-card .data-table th {
  padding: 0;
}
/* Sticky header within the scroll container so it stays visible as
   the user scrolls preview rows. Uses the same `--bg-sunken` as
   every other data-table header (and a bottom border) so the header
   row is visually distinct from the first body row — which it
   wasn't when we earlier overrode the background to `--bg-raised`
   (it merged with odd-zebra rows). The `top: 0` anchor is what
   differs from the global thead rule (which uses `top: 64px` to
   clear the topnav); inside this scroll container the sticky frame
   is the container itself. */
.builder-preview-card .data-table thead th {
  position: sticky;
  top: 0;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--line);
  z-index: 5;
}

/* ─────────── COMING SOON PAGE (comingsoon.html) ───────────
   Centered explanatory card rendered in place of the data table on
   any topnav module that doesn't yet have a real implementation —
   `dashboard`, `live`, `training`, `assessment`, `requests`, `settings`.
   The view inherits the surrounding chrome (topnav + client rail +
   branding panel) so the user still feels "inside" the system; only
   the main content area changes. The card provides:

     1. Module name + "Coming soon" headline
     2. One-sentence description ("we're working on it")
     3. Shortcuts to the modules that ARE live (Saved Tests, Reports)

   The two shortcut cards are anchor tags so middle-click / Cmd-click
   open in a new tab — same affordance as the topnav items they mirror. */
.coming-soon {
  display: grid;
  place-items: center;
  /* Fill the page area below the header. The exact subtraction is
     approximate (topnav 64px + page padding) — `place-items: center`
     handles the visual centering regardless. */
  min-height: calc(100vh - 200px);
  padding: 32px 16px 48px;
}
.coming-soon-card {
  width: 100%;
  max-width: 560px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 48px 40px 36px;
  box-shadow: var(--sh-md);
  text-align: center;
}
.coming-soon-icon {
  width: 72px;
  height: 72px;
  margin: 0 auto 20px;
  border-radius: 50%;
  background: var(--brand-tint);
  display: grid;
  place-items: center;
  /* The inline `Icon` SVG inherits `color` via `currentColor` on its
     stroke / fill, so brand-700 here tints the icon to match the
     brand-tinted disc behind it. */
  color: var(--brand-700);
}
.coming-soon-title {
  margin: 0 0 4px;
  font-size: var(--fsz-display);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.02em;
}
[dir="rtl"] .coming-soon-title { letter-spacing: 0; }
.coming-soon-module {
  margin: 0 0 14px;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--brand-700);
  letter-spacing: 0.01em;
  text-transform: uppercase;
}
[dir="rtl"] .coming-soon-module { text-transform: none; letter-spacing: 0; }
.coming-soon-sub {
  margin: 0 auto;
  max-width: 420px;
  font-size: var(--fsz-h3);
  color: var(--ink-2);
  line-height: 1.55;
}
[dir="rtl"] .coming-soon-sub { font-size: var(--fsz-h3); }

.coming-soon-divider {
  height: 1px;
  background: var(--line);
  margin: 32px -8px 24px;
}

.coming-soon-meanwhile {
  margin: 0 0 14px;
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
[dir="rtl"] .coming-soon-meanwhile { font-size: var(--fsz-body); text-transform: none; letter-spacing: 0; }

/* Shortcut cards for the live modules. Two-column grid that collapses
   to single-column on narrow viewports. Each card behaves like a
   button — hover lifts the surface to brand-tint, mirroring the
   topnav-item interaction pattern. */
.coming-soon-active-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.coming-soon-active-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  text-decoration: none;
  color: var(--ink);
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast), transform 80ms ease;
}
.coming-soon-active-card:hover {
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand-500) 35%, var(--line));
  color: var(--brand-700);
}
.coming-soon-active-card:active { transform: translateY(1px); }
.coming-soon-active-icon {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  color: var(--ink-2);
}
.coming-soon-active-card:hover .coming-soon-active-icon { color: var(--brand-700); }
.coming-soon-active-label {
  font-size: var(--fsz-h3);
  font-weight: 600;
}
/* Narrow viewport: stack shortcuts and tighten the card padding. */
@media (max-width: 480px) {
  .coming-soon-card { padding: 36px 24px 28px; }
  .coming-soon-title { font-size: var(--fsz-h1); }
  .coming-soon-active-grid { grid-template-columns: 1fr; }
}

/* ─────────── LOGIN PAGE (login.html) ───────────
   Lightweight standalone page — doesn't depend on React. Uses
   the same tokens (brand color, ink ramp, radii) as the rest of
   the app so the visual language stays consistent. The page is
   entered first; on submit, the user lands at yardsaved.html.
   `index.html` is a tiny redirect stub that bounces here so the
   GitHub Pages root URL still works. */
.auth-body {
  min-height: 100vh;
  background: var(--bg-sunken);
  background-image:
    radial-gradient(1200px 600px at 110% -10%, color-mix(in oklab, var(--brand-500) 18%, transparent), transparent 60%),
    radial-gradient(900px 600px at -10% 110%, color-mix(in oklab, var(--brand-500) 14%, transparent), transparent 60%);
  color: var(--ink);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
}
[dir="rtl"] .auth-body { font-family: var(--font-ui-ar, var(--font-ui)); }
/* Dark-mode gradient — same composition (top-right + bottom-left
   corner blooms) but the alpha needs to be ~2× the light-mode
   values to read as a clear brand presence. Two reasons the same
   percentages undershoot in dark:
     1. The radial center is parked at `110% -10%` / `-10% 110%`
        — *outside* the viewport. The visible corner only sees the
        gradient's falloff curve, so the on-screen alpha is roughly
        half of the stop value at the centre.
     2. Eye adaptation to a near-black canvas reads the result
        through a different gain curve — small Lab-L deltas that
        are obvious in light mode get absorbed by the dark surround.
   Pushing to 30% / 22% lands the visible portion of the bloom at
   a perceptual level that mirrors light mode, while the natural
   chroma damping from the alpha keeps it a soft tint, not a glow. */
[data-theme="dark"] .auth-body {
  background-image:
    radial-gradient(1200px 600px at 110% -10%, color-mix(in oklab, var(--brand-500) 24%, transparent), transparent 60%),
    radial-gradient(900px 600px at -10% 110%, color-mix(in oklab, var(--brand-500) 18%, transparent), transparent 60%);
}
.auth-page {
  flex: 1;
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 16px;
  padding: 20px 28px 16px;
  min-height: 100vh;
  /* Cap inner content width at 1600px (matching the dashboard
     `.topnav-inner` and `.main > *` content cap) while leaving the
     parent `.auth-body` to span the full viewport. The brand-color
     gradient on `.auth-body` continues edge-to-edge; only the
     header logos, the auth-card, and the footer links live inside
     this 1600px column, centered via `margin: 0 auto`. On viewports
     narrower than 1600px the cap has no effect — content just
     fills available width. */
  width: 100%;
  max-width: 1600px;
  margin-inline: auto;
  box-sizing: border-box;
}

/* Top bar — logo on the start side, theme + language toggles on
   the end. Matches the topnav's .tenant-icon sizing so the logo
   reads as the same brand mark seen throughout the app. */
.auth-top {
  display: flex;
  /* Top-align the two sides: the tenant logo (leading) and the trailing
     column — the second logo with the controls stacked beneath it — line up
     at the top. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
/* Trailing header column — the optional second logo with the lang/theme
   controls stacked under it (or just the controls when there's no second
   logo). */
.auth-top-end {
  display: flex;
  flex-direction: column;
  align-items: flex-end;   /* trailing edge: right in LTR, left in RTL */
  gap: 12px;
}
/* ── Login header never mirrors in RTL ───────────────────────────────────
   The header is fixed in every language, for EVERY tenant: tenant logo left,
   second logo + switchers right (switchers stacked under the second logo, or
   top-right when there's none). Co-branding lockups require it, and pinning it
   for all tenants keeps the header consistent — nothing in it swaps when the
   user toggles language. (The card + footer content still flow RTL in Arabic;
   only this top brand/utility bar is locked.) Only RTL needs overriding — LTR
   is already in this order. */
:root[dir="rtl"] .auth-top { flex-direction: row-reverse; }
:root[dir="rtl"] .auth-top-end { align-items: flex-start; }
:root[dir="rtl"] .auth-brand .tenant-icon { background-position: left center; }
:root[dir="rtl"] .auth-second-logo { background-position: right center; }
/* Keep the two switcher buttons in the same physical order (lang then theme)
   in both languages — an inline-flex reorders its children in RTL otherwise. */
:root[dir="rtl"] .auth-controls { flex-direction: row-reverse; }
/* Switchers are pinned to the physical right, so anchor their tooltips to the
   button's right edge (extend left, into view) rather than the RTL default. */
:root[dir="rtl"] .auth-controls [data-tip]::after { inset-inline-start: 0; inset-inline-end: auto; }
/* Second (partner / government) logo — login header trailing edge, for
   tenants that define one (boot.js sets --logo-second-* + the
   data-tenant-haslogo2 flag). Mirrors the tenant logo on the leading edge;
   hidden entirely when the active tenant has no second logo. */
.auth-second-logo {
  width: 132px;
  height: 40px;
  flex-shrink: 0;
  background-image: var(--logo-second-light);
  background-repeat: no-repeat;
  background-size: contain;
  background-position: right center;
}
[data-theme="dark"] .auth-second-logo { background-image: var(--logo-second-dark); }
[dir="rtl"] .auth-second-logo { background-position: left center; }
:root[data-tenant-haslogo2="false"] .auth-second-logo { display: none; }
/* Language + theme controls, relocated to just below the login card — a
   centered, subordinate row, clearly separated from the Sign-in CTA. */
/* Lang/theme controls now live in the footer (.auth-foot) — see the footer
   rules below. Their button-group styling is the base .auth-controls rule;
   the footer handles placement + the entrance animation. */
.auth-brand {
  display: flex;
  align-items: center;
  gap: 12px;
}
.auth-brand .tenant-icon {
  /* Match the expanded sidenav brand mark EXACTLY: `background-size: auto 40px`
     forces the logo to 40px tall, same as `.sidenav-brand-mark`. `contain`
     (the base) does NOT work here — in a fixed-width box it width-bounds a
     wide logo (the 5:1 RTA mark rendered only ~32px tall in the old 160px
     box). The box is 235px (like the sidenav) so the 40px-tall logo fits
     without clipping; left/right-center position comes from the base rule. */
  width: 235px;
  height: 40px;
  background-size: auto 40px;
}
.auth-controls {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.auth-ctl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 36px;
  min-width: 36px;
  padding: 0 12px;
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-2);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.auth-ctl:hover { background: var(--brand-tint); color: var(--brand-700); border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line)); }
.auth-ctl > svg { color: currentColor; }
/* Tooltips on the header controls anchor below + trailing edge — the buttons
   sit in the top-right (below the second logo), so below-anchoring clears the
   top edge and trailing alignment keeps the (wide) tip from clipping off the
   right edge. The unified `[data-tip]` base rule still provides the styling;
   we just override position. */
.auth-controls [data-tip]::after {
  bottom: auto;
  top: calc(100% + 6px);
  left: auto;
  right: auto;
  inset-inline-end: 0;
  transform: none;
}

/* Centered card */
.auth-main {
  /* Flex column (was grid place-items:center) so the card + the controls
     row beneath it stack as one centered group — otherwise the grid spread
     them into separate auto-rows with a large gap. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px 0;
  min-height: 0;
}
.auth-card {
  width: 100%;
  /* Tightened from 460px → 420px so the card feels focused. The
     padding + gap below were tuned together with the removal of the
     "Welcome back" / "Sign in to continue." copy block — total
     vertical height drops by ~70px, fitting comfortably on
     iPad-mini-landscape and similar low-height viewports. */
  max-width: 420px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 28px 28px 24px;
  box-shadow: var(--sh-lg);
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.auth-head {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* Subtitle was removed but the gap between the welcome heading and
     the username field needs to stay where it was — preserves visual
     rhythm of the form. The previous subtitle contributed roughly
     22px (4px head-gap + ~18px line-height) below the title. */
  padding-bottom: 22px;
}
/* Title-row layout — welcome heading on the leading edge, platform
   logo on the trailing edge. `space-between` is writing-direction
   aware, so RTL mirroring is automatic. `flex-end` on the cross
   axis aligns the logo's bottom to the title's descender baseline. */
.auth-head-top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
}

/* Platform brand mark inside the auth card (production layout) —
   small companion mark next to the welcome title. Tenant logo in
   the page header (.tenant-icon) is the company brand; this is
   the platform brand. The asset is theme-swapped via the same
   pattern as .tenant-icon. */
.auth-product-logo {
  width: 66px;
  height: 24px;
  flex-shrink: 0;
  /* System (product) logo — tenant-driven via --logo-system-* (iVE / STS). */
  background-image: var(--logo-system-light, url('assets/tenants/pl/ive-light.svg'));
  background-repeat: no-repeat;
  background-size: contain;
  background-position: center;
}
[data-theme="dark"] .auth-product-logo {
  background-image: var(--logo-system-dark, url('assets/tenants/pl/ive-dark.svg'));
}
.auth-title {
  margin: 0;
  font-size: var(--fsz-h1);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--ink);
}
[dir="rtl"] .auth-title { letter-spacing: 0; }
.auth-sub {
  margin: 0;
  /* Label tier (12px) — the system name reads as a quiet subtitle
     to the primary "Welcome back" greeting. */
  font-size: var(--fsz-label);
  color: var(--ink-3);
  line-height: 1.4;
}
[dir="rtl"] .auth-sub { font-size: var(--fsz-label); }

.auth-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.auth-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink-2);
}
[dir="rtl"] .auth-label { font-size: var(--fsz-label); }
.auth-input {
  display: block;
  width: 100%;
  padding: 10px 12px;
  font-family: inherit;
  font-size: var(--fsz-body);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--t-fast), box-shadow var(--t-fast), background var(--t-fast);
  box-sizing: border-box;
}
.auth-input::placeholder { color: var(--ink-4); }
.auth-input:hover { border-color: var(--line-strong); }
.auth-input:focus {
  outline: 0;
  /* Two-layer halo — matches `.filter-search:focus-within` (the
     Saved Tests / Reports filter-bar search, which is the unified
     system pattern). Inner 1px shadow at 35% brand-500 draws the
     visible edge (replaces a border-color change). Outer 4px halo
     at `--brand-tint` (= brand-600 @ 6% transparent) is the soft
     system glow. The element's `border` stays --line (resting)
     so there's no border-color jump at focus — just the shadow
     stacking on top. Tied to --brand-tint so the focus halo
     auto-aligns with hover/active surfaces throughout the app. */
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--brand-500) 35%, var(--line)),
    0 0 0 4px var(--brand-tint);
}

.auth-link {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--brand-700);
  text-decoration: none;
  /* Unified inline-link treatment — see "Inline link / CTA standard"
     in the shared block below. Hover: darken color + soft brand tint
     background, never underline. */
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.auth-link:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.auth-link:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* Password input + show/hide toggle on the trailing side. The toggle
   sits 1px inside the input's border (top/bottom/end) so the hover
   background reaches the input's edge and the trailing corners can
   match the input's outer radius — the result is a clean rounded
   "tab" that fills the slot rather than a small floating chip. */
.auth-pwd-wrap { position: relative; }
.auth-pwd-toggle {
  position: absolute;
  top: 1px;
  bottom: 1px;
  inset-inline-end: 1px;
  width: 40px;
  display: inline-grid;
  place-items: center;
  background: transparent;
  border: 0;
  color: var(--ink-3);
  cursor: pointer;
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  border-start-end-radius: calc(var(--r-md) - 1px);
  border-end-end-radius: calc(var(--r-md) - 1px);
  transition: background var(--t-fast), color var(--t-fast);
}
.auth-pwd-toggle:hover { background: var(--bg-sunken); color: var(--ink); }
.auth-pwd-wrap .auth-input { padding-inline-end: 44px; }

.auth-remember {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-label);
  color: var(--ink-2);
  cursor: pointer;
  user-select: none;
}
.auth-remember input { accent-color: var(--brand-600); }

/* Sign-in button — full-width, primary. */
.auth-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 16px;
  font-family: inherit;
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--brand-ink);
  background: var(--brand-cta);
  border: 1px solid var(--brand-cta);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), transform 80ms ease;
}
.auth-submit:hover { background: color-mix(in oklab, var(--brand-cta), #000 8%); border-color: color-mix(in oklab, var(--brand-cta), #000 8%); }
.auth-submit:active { transform: translateY(1px); }
.auth-submit:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* Inline error banner — sits between the heading and the email field
   when credentials don't match. Hidden via `is-hidden` rather than
   removed from the DOM so screen readers see the live region update. */
.auth-error {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: color-mix(in oklab, var(--err) 10%, var(--bg-sunken));
  border: 1px solid color-mix(in oklab, var(--err) 35%, var(--line));
  border-radius: var(--r-md);
  font-size: var(--fsz-label);
  color: color-mix(in oklab, var(--err) 70%, var(--ink));
  line-height: 1.4;
}
[dir="rtl"] .auth-error { font-size: var(--fsz-body); }
.auth-error.is-hidden { display: none; }
.auth-error-mark {
  width: 20px; height: 20px;
  flex-shrink: 0;
  border-radius: 50%;
  background: color-mix(in oklab, var(--err) 18%, transparent);
  color: var(--err);
  display: inline-grid;
  place-items: center;
}
/* When credentials are rejected, both fields take a red border so the
   user immediately sees the form is in an error state. Cleared on the
   first `input` event in either field. Solid `--err` (same cleanup
   as the focus-border above) — the previous `color-mix(err 55%, line)`
   read as a muddied pinkish-grey rather than a clear error signal. */
.auth-input.is-error {
  border-color: var(--err);
}
.auth-input.is-error:focus {
  /* Same two-layer halo vocabulary as `.auth-input:focus` above, but
     red-tinted so the error state still wins over the focus brand
     tint. Inner edge at 50% err + outer halo at err @ 8% transparent
     (slightly stronger than --brand-tint's 6% so the error reads as
     "this needs attention" against the resting state). The border
     stays --err from .is-error above; the shadow stacks on top. */
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--err) 50%, var(--line)),
    0 0 0 4px color-mix(in oklab, var(--err) 8%, transparent);
}

/* Per-field validation message — sits 4px below the input when the
   field is empty on submit. The slot's vertical space is *always*
   reserved (via `visibility: hidden` rather than `display: none` when
   inactive) so showing the message doesn't push the form taller and
   shift everything below it. The slot uses `min-height` to lock the
   space at exactly one line so the reservation is consistent whether
   the message is currently visible or not. */
.auth-field-error {
  margin-top: 4px;
  min-height: 1.35em;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--err);
  line-height: 1.35;
}
[dir="rtl"] .auth-field-error { font-size: var(--fsz-label); }
.auth-field-error.is-hidden { visibility: hidden; }

/* "Need access?" help line below the submit button — directs
   first-time visitors to their administrator. */
.auth-help {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: var(--fsz-label);
  color: var(--ink-3);
  padding-top: 2px;
}
[dir="rtl"] .auth-help { font-size: var(--fsz-label); }

/* Footer — copyright + Privacy / Terms / Support links. */
.auth-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  padding: 4px 0 8px;
}
[dir="rtl"] .auth-foot { font-size: var(--fsz-label); }
.auth-foot a {
  color: var(--brand-700);
  text-decoration: none;
  font-weight: 600;
  /* Unified inline-link treatment — color shift + soft brand bg
     tint on hover, never underline. */
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.auth-foot a:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.auth-foot a:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
.auth-foot-sep { color: var(--ink-4); user-select: none; }

/* Smaller phones: drop card padding, top bar tightens. */
@media (max-width: 480px) {
  .auth-page { padding: 14px 16px; }
  .auth-card { padding: 24px 22px 22px; max-width: 100%; }
  .auth-title { font-size: var(--fsz-h2); }
  /* Logos shrink for the narrow header but stay matched in height (tenant +
     second logo both ~33px), and both fit side by side down to 320px. */
  .auth-brand .tenant-icon { width: 170px; height: 33px; background-size: auto 33px; }
  .auth-second-logo { width: 109px; height: 33px; }
}

/* ─────────── LOGIN PAGE — entrance animation ───────────
   Pure-CSS staggered fade+slide-up for the auth-page's three rows
   (header, card, footer). Plays once on initial paint, respects
   `prefers-reduced-motion`. The card itself uses a slightly larger
   slide so it's the visual anchor of the entrance. */
@keyframes auth-fade-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes auth-fade-up-card {
  from { opacity: 0; transform: translateY(20px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.auth-top      { animation: auth-fade-up      420ms cubic-bezier(0.16, 1, 0.3, 1) backwards; animation-delay: 60ms; }
.auth-card     { animation: auth-fade-up-card 540ms cubic-bezier(0.16, 1, 0.3, 1) backwards; animation-delay: 140ms; }
/* The lang/theme controls now live inside .auth-foot, so the footer's
   animation carries them — no separate rule needed (one less thing to keep
   in sync). The footer fades up last in the cascade. */
.auth-foot     { animation: auth-fade-up      420ms cubic-bezier(0.16, 1, 0.3, 1) backwards; animation-delay: 320ms; }
@media (prefers-reduced-motion: reduce) {
  .auth-top, .auth-card, .auth-foot { animation: none; }
}

/* (LOADING OVERLAY block removed — was tied to LoadingOverlay
   React component that no longer exists. Login uses an inline
   button "Signing in…" state; every other page boots through its
   pre-React skeleton.) */

/* ─────────── BRANDING PANEL ───────────
   Class prefix is `.tweaks-*` for historical reasons (the panel
   was formerly named TweaksPanel). Class names are internal-only
   — keep them stable so existing CSS hooks aren't broken. */
.tweaks-overlay {
  position: fixed; inset: 0;
  background: color-mix(in oklab, var(--ink) 30%, transparent);
  /* Top-level settings drawer — must sit above ALL page content. Was
     z-index:100 (dropdown tier), which let the test-canvas in-vehicle
     widget (`.tc-invehicle`, z-index:1000) poke through it. Clear the
     modal tier so the drawer covers the canvas chrome; still below
     toasts (1500) / tooltips (2000). */
  z-index: calc(var(--z-modal, 1000) + 10);
  display: flex;
  justify-content: flex-end;
  align-items: stretch;
  animation: fade-in 180ms ease;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.tweaks-panel {
  width: 380px;
  max-width: 100%;
  background: var(--bg-raised);
  border-inline-start: 1px solid var(--line);
  display: flex; flex-direction: column;
  box-shadow: var(--sh-lg);
  animation: slide-in 240ms cubic-bezier(.4,0,.2,1);
}
@keyframes slide-in { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } }
[dir="rtl"] .tweaks-panel { animation-name: slide-in-rtl; }
@keyframes slide-in-rtl { from { transform: translateX(-20px); opacity: 0; } to { transform: none; opacity: 1; } }

.tweaks-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
}
.tweaks-title {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--fsz-body); font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
}

.tweaks-body {
  padding: 4px 16px 24px;
  overflow-y: auto;
  flex: 1;
}

.tweak-sec {
  padding: 16px 0;
  border-bottom: 1px solid var(--line);
}
.tweak-sec:last-of-type { border-bottom: 0; }

.tweak-sec-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 12px;
}
.tweak-sec-title {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.tweak-sec-value {
  font-size: var(--fsz-caption);
  color: var(--ink-4);
}

/* Swatch grid */
.swatch-grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 6px;
  margin-bottom: 12px;
}
.swatch {
  aspect-ratio: 1;
  border-radius: var(--r-sm);
  border: 0;
  cursor: pointer;
  position: relative;
  transition: transform var(--t-fast);
}
.swatch:hover { transform: scale(1.08); }
.swatch.is-on::after {
  content: '';
  position: absolute; inset: -3px;
  border: 2px solid var(--ink);
  border-radius: calc(var(--r-sm) + 3px);
}

/* .seg / .seg-opt — retired. Use .seg-toggle.is-compact (.is-stretch
   for narrow drawers/panels). The canonical class lives next to the
   token block so all toggle treatments are co-located. */

.hex-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
.hex-color {
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: transparent;
  cursor: pointer;
}
.hex-color::-webkit-color-swatch { border: 0; border-radius: calc(var(--r-sm) - 2px); }
.hex-color::-webkit-color-swatch-wrapper { padding: 2px; }
.hex-text {
  flex: 1;
  min-width: 0;
  padding: 6px 10px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fsz-label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.hex-text:focus {
  outline: 2px solid var(--ring);
  outline-offset: 0;
  border-color: var(--line-strong);
}

/* Mobile drawer (hamburger menu) */
.mobile-overlay {
  position: fixed;
  inset: 0;
  background: color-mix(in oklab, var(--ink) 35%, transparent);
  z-index: 130;
  animation: fade-in 180ms ease;
}
.mobile-drawer {
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  width: 300px;
  max-width: 86vw;
  height: 100vh;
  background: var(--bg-raised);
  border-inline-end: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  box-shadow: var(--sh-lg);
  animation: drawer-slide 220ms cubic-bezier(.4,0,.2,1);
}
[dir="rtl"] .mobile-drawer { animation-name: drawer-slide-rtl; }
@keyframes drawer-slide { from { transform: translateX(-100%); } to { transform: none; } }
@keyframes drawer-slide-rtl { from { transform: translateX(100%); } to { transform: none; } }

.mobile-drawer-close {
  /* sm icon-only tier — see tokens.css `--cta-h-*`. */
  position: absolute;
  top: 10px;
  inset-inline-end: 14px;
  width: var(--cta-h-sm); height: var(--cta-h-sm);
  display: inline-grid;
  place-items: center;
  padding: 0;
  z-index: 1;
}
.mobile-drawer-body { flex: 1; overflow-y: auto; padding: 32px 8px 8px; }
.mobile-drawer-section { padding: 8px 0; }
.mobile-drawer-section + .mobile-drawer-section { border-top: 1px solid var(--line); margin-top: 8px; }
.mobile-nav-item {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  padding: 10px 12px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fsz-body);
  font-weight: 500;
  text-align: start;
  text-decoration: none;          /* anchor variants */
}
.mobile-nav-item:hover { background: var(--brand-tint); color: var(--ink); }
.mobile-nav-item.is-active { background: var(--brand-tint-2); color: var(--brand-700); }
[data-theme="dark"] .mobile-nav-item.is-active { color: var(--brand-700); }

/* ─────────── Responsive ─────────── */
/* ≤ 1280: compact-desktop / large-tablet — sidebar shrinks, nav becomes
   icon-only, filter bar wraps to 2 rows. Sidebar still visible so the
   user can browse clients without opening a drawer. */
/* ≤ 1279: topnav grid tightens. Filter-bar wrapping moved to the
   ≤1079 query so the search + pills stay on one row across the full
   1080–1279 zone (the pills + density actions fit comfortably until
   the icons start losing room around 1079). */
@media (max-width: 1279px) {
  /* Brand cell stays locked to the logo's 128px width — the previous
     140px reservation left a 12px empty strip that the nav items had
     to skip past. Holding it at 128 gives the middle nav row that
     extra space without changing the logo's footprint. */
  .topnav-inner { grid-template-columns: 128px 1fr auto; }
}

/* ≤ 1079: top-nav text-only (icons hidden) and filter pill icons
   drop so the labels carry the full pill width. The filter bar
   itself stays on a single row at this range — pills + search keep
   sharing the bar; only the per-pill leading icon is hidden. The
   bar's wrap behaviour is moved down to the ≤899 query. */
@media (max-width: 1100px) {
  .nav-item > svg { display: none; }
  .nav-item { padding: 6px 8px; }

  /* Pill icons drop. Pill labels carry the full width at this density. */
  .filter-pill > svg:first-of-type { display: none; }
  .filter-pill { padding-inline-start: 12px; }

  /* Saved Tests freezes its columns at the 1080-state width (1080 −
     32 main padding − 2 card border = 1046) and the wrapper scrolls
     horizontally. Reports tables (`.rpt-table-wrap`) DO NOT need a
     forced min-width here: once `.rpt-split` collapses to one column
     at this viewport they get the full doc width and fit naturally,
     so we deliberately leave them alone. Tables that need their own
     freeze (Custom Reports templates, Schedules) declare their
     min-width on their specific class outside this query. */
  .table-scroll { overflow-x: auto; overflow-y: visible; }
  .table-scroll .data-table { min-width: 1046px; }
  .table-scroll .data-table thead th { position: static; }

  /* Schedules freezes the same way Saved Tests / Custom Reports do.
     `.rpt-table-wrap.sched-table-wrap` (3 classes) overrides the
     ≤899 generic 866 freeze on `.rpt-table-wrap .data-table`, so
     Schedules holds its 1080-state width across the entire 0–1079
     range instead of stepping down at 899. */
  .rpt-table-wrap.sched-table-wrap { overflow-x: auto; overflow-y: visible; }
  .rpt-table-wrap.sched-table-wrap .sched-table { min-width: 1046px; }
  .rpt-table-wrap.sched-table-wrap .sched-table thead th { position: static; }

  /* Maneuver L1 (and any other 2-pane split layout) collapses to
     a single column at this viewport so each table gets the full
     doc width without horizontal scroll, instead of being squeezed
     into a ~520px pane that can't display 5–6 columns. */
  .rpt-split { grid-template-columns: 1fr; }

  /* Builder fields drop from 3 → 2 columns at the freeze. */
  .fields-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Step 7 Run-at field wraps to its own row at this viewport ONLY
     when the optional day picker is present (Weekly → "Day of the
     week", Monthly → "Day of the month"). Schedule + Day + Run-at
     together can't fit one row at ≤1079, so Run-at drops below.
     When the day picker isn't showing (e.g. Daily / "On test
     completion" presets), there's plenty of room for Schedule +
     Run-at side-by-side, so we leave them on row 1. The `:has()`
     selector flips the rule on/off based on the actual children
     rendered. */
  .builder-sched-row:has(.builder-sched-day) .builder-sched-time { flex-basis: 100%; }
}

/* ≤ 720: mobile — sidebar hidden, hamburger drawer carries nav and
   clients, table scrolls horizontally to keep readable columns.
   Filter-bar restructure is now content-aware (container query at
   the top of this file) so it's NOT duplicated here. */
@media (max-width: 720px) {
  .shell { grid-template-columns: 1fr; }
  /* Rail keeps working as an overlay on mobile, just narrower so it
     doesn't cover the whole viewport. The school-switcher button in the
     page header remains the primary way to open it. */
  .client-rail { width: min(296px, 88vw); }
  /* `.main` padding stays at 16px (set above 1100) so the visible
     table-card width doesn't jump at this breakpoint. The table is
     already frozen at 1046 by the 1100 rule; changing main padding
     here would shift the card edge and read as a column change
     visually. */
  /* School switcher stays visible and usable in mobile */
  .school-switcher { min-width: 0; max-width: 100%; }
  .topnav-inner {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .topnav-brand { flex-shrink: 0; }
  .hamburger-btn { display: inline-grid; flex-shrink: 0; }
  .topnav-items { display: none; }
  .topnav-tools { margin-inline-start: auto; flex-shrink: 0; }
  /* Table-scroll/min-width rules for Saved Tests live in the 1100
     block above so the 720–1100 zone benefits too. */

  /* Reports tables freeze and scroll horizontally below this
     breakpoint. The wrapper gets overflow-x and the table has a
     min-width so columns don't keep squeezing into unreadable
     widths. Applies to Institute L1/L2/L3, Maneuver L1/L2,
     Examiner L1/L2 and any other report table that uses
     `.rpt-table-wrap` as its wrapper. */
  .rpt-table-wrap { overflow-x: auto; overflow-y: visible; }
  .rpt-table-wrap .data-table { min-width: 866px; }
  .rpt-table-wrap .data-table thead th { position: static; }

  /* Test-Analysis split-pane tables (`.rpt-split` children) keep
     a tighter min-width — they live inside a 2-pane layout that
     collapses to one column at ≤1100, so they already get the
     full doc width above this breakpoint. Below 720 they freeze
     and scroll like any other rpt table, just at a smaller width.
     Applies to Maneuver Performance Summary (L1), Top 10 Failed
     Parameters (L1), Failed parameters in <maneuver> (L2), and
     Failure rate by branch (L2). */
  .rpt-split .rpt-table-wrap .data-table { min-width: 515px; }

  /* Notification panel becomes a side drawer in responsive */
  .notif-panel {
    position: fixed;
    top: 0;
    inset-inline-end: 0;
    width: 100%;
    max-width: 380px;
    height: 100vh;
    /* Use dvh on mobile so 100% of the height accounts for the
       dynamic viewport (browser chrome shows/hides on iOS Safari) —
       without it the panel can extend below the visible area and
       its close button drops out of reach. */
    height: 100dvh;
    max-height: none;
    border-radius: 0;
    border-inline-start: 1px solid var(--line);
    border-inline-end: 0;
    border-top: 0;
    border-bottom: 0;
    z-index: 200;
    animation: drawer-slide-end 220ms cubic-bezier(.4,0,.2,1);
  }
  /* Force the close button visible on mobile drawer mode — it's
     the primary exit path here (the backdrop strip outside the
     380px panel is too narrow to be reliably tappable). */
  .notif-close-btn {
    display: inline-flex !important;
  }
  [dir="rtl"] .notif-panel { animation-name: drawer-slide-start; }
  @keyframes drawer-slide-end { from { transform: translateX(100%); } to { transform: none; } }
  @keyframes drawer-slide-start { from { transform: translateX(-100%); } to { transform: none; } }
  .notif-backdrop {
    position: fixed;
    inset: 0;
    background: color-mix(in oklab, var(--ink) 40%, transparent);
    z-index: 199;
    animation: fade-in 180ms ease;
  }

  /* Profile menu becomes a side drawer in responsive */
  .profile-menu {
    position: fixed;
    top: 0;
    inset-inline-end: 0;
    width: 100%;
    max-width: 320px;
    height: 100vh;
    max-height: none;
    border-radius: 0;
    border-top: 0;
    border-bottom: 0;
    border-inline-end: 0;
    border-inline-start: 1px solid var(--line);
    padding: 24px 12px 12px;
    z-index: 200;
    animation: drawer-slide-end 220ms cubic-bezier(.4,0,.2,1);
  }
  [dir="rtl"] .profile-menu { animation-name: drawer-slide-start; }
  .profile-backdrop {
    position: fixed;
    inset: 0;
    background: color-mix(in oklab, var(--ink) 40%, transparent);
    z-index: 199;
    animation: fade-in 180ms ease;
  }

  /* Saved-Tests table scroll is owned by the 1100 query so the
     720–1100 tablet zone benefits too. */
}

/* ─────────── REPORTS — FILTER ACTIONS, KPI SEP, PR CELL TIP, OVERRIDE BANNER, DRILL HINT ─────────── */
/* Reports filter bar — Apply pinned to the far end (right in LTR) on the
   same row as the pills. The base .filter-bar has a ≤1279px media query
   that orders .filter-actions before .filter-pills (used by Saved Tests
   where Apply lives in a fixed-width search/density row). For Reports we
   want the opposite — pills first, Apply at the end — so we override the
   order + flex-wrap explicitly at all breakpoints. */
.filter-bar--reports {
  flex-wrap: nowrap;
  /* Pin filters to the top of the bar so the pill row sits aligned
     with the top-left corner regardless of how tall the bar grows
     (multi-row pills, stacked actions). The base `.filter-bar` rule
     uses `align-items: center` which would otherwise push the pill
     row down toward the vertical center as soon as anything else in
     the bar (e.g. stacked actions) makes it taller. */
  align-items: flex-start;
  /* Match the padding shape of `.rpt-head` (16px 18px) so the filter
     bar reads as a sibling card with the same internal whitespace. */
  padding: 16px 18px;
}
.filter-bar--reports .filter-pills {
  flex: 1 1 0;
  min-width: 0;
  /* Pills wrap internally if there are too many to fit; the pills div
     becomes taller but Apply still sits beside it on the bar's row. */
  flex-wrap: wrap;
  /* Reset any order assigned by the base mobile media query. */
  order: 0;
}
.filter-actions--reports {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  /* Top-align to match the pill row's pinned-to-top behavior — the
     parent's `align-items: flex-start` already defaults children here,
     this is just the explicit cue. */
  align-self: flex-start;
  /* Override the base ≤1279px rule (.filter-actions { order: 1 }) which
     would otherwise render Apply before the pills — i.e. on the LEFT. */
  order: 1;
  margin-inline-start: auto;
}
.filter-actions--reports .btn,
.filter-actions--reports .filter-reset { align-self: center; }
/* Below 1080 the InstitutePerformance pill row gets tight enough that
   adding the side-by-side "Unapplied changes" hint pushes pills onto a
   2nd line. At that point we stack the actions vertically — hint on
   top, Apply pinned beneath it, both right-aligned — so the actions
   column shrinks to ~Apply-button width and the pills row reclaims
   its horizontal space. Source order is unchanged (hint first, Apply
   second) so the visual order matches: hint above Apply. */
@media (max-width: 1100px) {
  .filter-actions--reports {
    flex-direction: column;
    align-items: flex-end;
    align-self: flex-start;
  }
  .filter-actions--reports .btn,
  .filter-actions--reports .filter-reset { align-self: flex-end; }
}
@media (max-width: 720px) {
  /* Narrow viewports: let the bar wrap so Apply drops below the pills
     cleanly. Apply still pushed to the end via margin-inline-start: auto. */
  .filter-bar--reports { flex-wrap: wrap; }
}

/* Very dense viewports (≤439): the filter pills and the actions
   group can no longer share a row, and the rpt-head's title + action
   CTAs need to stack vertically too. Both surfaces drop their
   trailing actions onto their own row below the rest of the content. */
@media (max-width: 480px) {
  /* Filter bar: actions claim a full-width row beneath the pills. The
     ≤1079 rule already stacks the hint above the Apply button inside
     `.filter-actions--reports` and right-aligns them; the only thing
     this width: 100% adds is forcing the group onto its own line
     instead of sitting beside whatever's left of the pills. */
  .filter-bar--reports { flex-wrap: wrap; }
  .filter-actions--reports { width: 100%; }

  /* Rpt-head: title + level chain occupy the top, action CTAs
     (Generate report / Schedule) stack below them. Column flex on
     `.rpt-head-row` does the row-stack; `.rpt-head-actions` already
     stacks vertically from the ≤559 container query (Generate Report
     on top, Schedule below). */
  .rpt-head-row {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }
}

/* Pending pill — user changed this filter but hasn't clicked Apply yet.
   Dashed amber outline distinguishes "selected but pending" from the solid
   brand-tinted "selected and applied" state. The dashed border is what the
   user reads as "this isn't live yet". */
.filter-pill.is-pending {
  border-color: var(--warn, oklch(0.72 0.15 75));
  border-style: dashed;
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 8%, var(--bg-raised));
  color: var(--ink);
}
.filter-pill.is-pending:hover { background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 14%, var(--bg-raised)); }
/* When a pill is BOTH active and pending, keep the dashed pending outline
   on top — pending is the more important signal until the user applies. */
.filter-pill.is-active.is-pending {
  border-color: var(--warn, oklch(0.72 0.15 75));
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 12%, var(--bg-raised));
  color: var(--ink);
}

/* Inline hint shown next to the Apply button while there are staged but
   unapplied changes. Disappears the moment the user clicks Apply. */
.filter-pending-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 75%, var(--ink));
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 12%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 35%, var(--line));
  border-radius: var(--r-md);
  white-space: nowrap;
  animation: hint-fade-in 180ms ease-out;
}
[dir="rtl"] .filter-pending-hint { font-size: var(--fsz-body); }
.filter-pending-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--warn, oklch(0.72 0.15 75));
  flex-shrink: 0;
  animation: hint-pulse 1.6s ease-in-out infinite;
}
@keyframes hint-fade-in {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes hint-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 50%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 0%, transparent); }
}

/* Apply button while changes are pending — subtle brightness / shadow lift
   so the eye lands on it. Doesn't change the disabled state colour because
   pending implies the button is enabled. */
.btn.btn-attention {
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-500) 22%, transparent), var(--sh-xs);
}

/* Inline separator inside a KPI value — used for "1 of 3" so "of" reads as a
   light grammatical connector rather than a number. */
.kpi-value-sep {
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink-4);
  letter-spacing: 0;
  margin: 0 2px;
}
/* "of N" trailer — keeps the headline number ("1") at full kpi-value size
   and pushes "of 3" into a small, muted segment to its right at a single
   uniform smaller size for both "of" and the total. */
.kpi-value-of {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
  letter-spacing: 0;
  margin-inline-start: 6px;
  font-variant-numeric: tabular-nums;
}
/* RTL bump for .kpi-value-of removed: was forcing the "of N" trailer
   to body size (15px) which broke the size relationship to its sibling
   (the headline number stays at h1, the trailer should stay at the
   label tier). The natural RTL token bump (--fsz-label 12→13) is
   sufficient. */

/* PrCell breakdown — INNER content selectors only.
   The outer popover chrome (bg/border/shadow/position/arrow) is now
   provided by the unified `<ChartTip>` wrapper that PrCell renders
   into. These selectors style the rows/labels/numbers inside that
   wrapper. The legacy `.pr-cell-tip-host` host wrapper + the
   `.pr-cell .pr-cell-tip` scoped chrome have both been removed —
   neither was reachable from any JSX. */
.pr-cell-tip-row { display: contents; }
.pr-cell-tip-head {
  color: var(--tooltip-fg-muted);
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.pr-cell-tip-label { font-weight: 600; font-size: var(--fsz-caption); color: var(--tooltip-fg-muted); }
/* Numeric values in the tooltip — `.data-table .mono` (the cell host)
   would otherwise force these spans to 14px body size, but inside this
   11px caption-tier popover they must read at the popover's baseline. */
.pr-cell-tip .mono {
  font-size: var(--fsz-caption);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--tooltip-fg);
  letter-spacing: 0;
}

/* Override-request status banner — single-line layout: status dot + label,
   reviewer/date in the middle, "View request" CTA pinned to the end. Tone
   is set by a modifier class (.is-info / .is-pass / .is-warn / .is-fail). */
.override-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
  padding: 10px 14px;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  background: var(--bg-raised);
  font-size: var(--fsz-body);
  line-height: 1.4;
}
.override-banner-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--ink-3);
  flex-shrink: 0;
}
.override-banner-label { font-weight: 600; color: var(--ink); }
.override-banner-meta { color: var(--ink-3); font-size: var(--fsz-label); font-weight: 500; }
/* "View request" CTA — pinned to the trailing edge of the banner via auto
   margin so it stays right-aligned regardless of the meta text length. */
.override-banner-cta {
  /* sm CTA tier — 4×10 padding aligned to the canonical `.btn-sm`
     (was 5×10, off the system scale). */
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-inline-start: auto;
  padding: 4px 10px;
  background: var(--bg-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  font-family: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  transition: all var(--t-fast);
}
.override-banner-cta:hover {
  background: var(--bg-sunken);
  border-color: var(--ink-3);
}
.override-banner-cta svg { color: var(--ink-3); }
.override-banner.is-info  { border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line)); background: var(--brand-tint); }
.override-banner.is-info  .override-banner-dot { background: var(--brand-600); box-shadow: 0 0 0 4px color-mix(in oklab, var(--brand-500) 20%, transparent); animation: ovr-pulse 1.6s ease-in-out infinite; }
.override-banner.is-pass  { border-color: color-mix(in oklab, var(--ok) 30%, var(--line)); background: color-mix(in oklab, var(--ok) 8%, var(--bg-raised)); }
.override-banner.is-pass  .override-banner-dot { background: var(--ok); }
.override-banner.is-warn  { border-color: color-mix(in oklab, var(--warn) 35%, var(--line)); background: color-mix(in oklab, var(--warn) 10%, var(--bg-raised)); }
.override-banner.is-warn  .override-banner-dot { background: var(--warn); }
.override-banner.is-fail  { border-color: color-mix(in oklab, var(--err) 30%, var(--line)); background: color-mix(in oklab, var(--err) 8%, var(--bg-raised)); }
.override-banner.is-fail  .override-banner-dot { background: var(--err); }
@keyframes ovr-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--brand-500) 30%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in oklab, var(--brand-500) 0%, transparent); }
}

/* Drill hint — finger-pointer icon + label. The icon already signals "tap
   to open detail"; the surrounding hint stays small and inline so it sits
   quietly above the table. */
.rpt-drill-hint svg { color: var(--brand-600); }
[data-theme="dark"] .rpt-drill-hint svg { color: var(--brand-700); }

/* L4 student card — sits between the document head and the KPI strip.
   Photo on the inline-start, name + identity grid alongside. Mirrors the
   density of the saved-tests popover but as a static block on the page. */
.l4-student-card {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 14px 16px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-xs);
}
.l4-student-photo {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bg-sunken);
  flex-shrink: 0;
}
.l4-student-info { flex: 1; min-width: 0; }
.l4-student-name {
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.2;
}
[dir="rtl"] .l4-student-name { letter-spacing: 0; }
.l4-student-name-sub {
  /* This subline holds the OPPOSITE-language spelling of the name:
     in LTR (English) mode it shows the Arabic name; in RTL (Arabic)
     mode it shows the English name. Use --font-ar so Arabic glyphs
     render in IBM Plex Sans Arabic regardless of mode (Inter Tight
     has no Arabic glyphs and falls back to a system font that
     breaks visual consistency). The block aligns to the page's
     start side (left in LTR, right in RTL) so it always sits
     directly under the primary name above. Direction inherits from
     the page so a Latin subline in RTL flushes right and an Arabic
     subline in LTR flushes left — Unicode bidi handles per-char
     direction within the line. */
  font-family: var(--font-ar);
  font-size: var(--fsz-body);
  color: var(--ink-3);
  font-weight: 400;
  line-height: 1.3;
  margin-top: 2px;
  text-align: start;
}
.l4-student-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 6px 18px;
  margin: 10px 0 0;
}
.l4-student-grid > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.l4-student-grid dt {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
}
[dir="rtl"] .l4-student-grid dt { font-size: var(--fsz-caption); letter-spacing: 0; text-transform: none; }
.l4-student-grid dd {
  margin: 0;
  font-size: var(--fsz-body);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Inline copy-icon variant — same flex pattern the profile popover uses
   for mobile / email. The icon sits flush after the value, separated by
   a small gap; the value can still ellipsis-truncate if it doesn't fit. */
.l4-student-grid dd.l4-student-grid-copy {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  overflow: visible;
}
.l4-student-grid dd.l4-student-grid-copy > span:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.profile-vehicle-cat { color: var(--ink-3); }

/* ─────────── GENERATE-REPORT MODAL ─────────── */
.modal-backdrop {
  position: fixed; inset: 0;
  background: color-mix(in oklab, var(--ink) 50%, transparent);
  backdrop-filter: blur(2px);
  z-index: 300;
  display: grid; place-items: center;
  padding: 16px;
  animation: fade-in 160ms ease;
}
.modal {
  width: 100%;
  max-width: 520px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-lg);
  display: flex; flex-direction: column;
  /* dvh accounts for mobile browser chrome — without it, the
     modal's vh-based max-height can exceed the visible area
     (iOS Safari URL bar shows/hides), pushing the modal-foot
     below the fold and making Save/Cancel buttons unreachable. */
  max-height: calc(100dvh - 32px);
  animation: gen-modal-in 200ms cubic-bezier(.4,0,.2,1);
}
@keyframes gen-modal-in {
  from { transform: translateY(8px) scale(0.985); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  /* Brand-tinted header band (tokens.css --modal-head-*). Tones below
     swap the hue for semantic modals. Top corners rounded to nest
     inside the modal's --r-lg radius (the modal has no overflow clip). */
  background: var(--modal-head-bg);
  border-bottom: 1px solid var(--modal-head-line);
  border-start-start-radius: calc(var(--r-lg) - 1px);
  border-start-end-radius: calc(var(--r-lg) - 1px);
}
/* Semantic tones — add .is-danger / .is-warn / .is-success on the
   .modal-head (destructive confirms use .is-danger). */
.modal-head.is-danger  { background: var(--modal-head-bg-danger);  border-bottom-color: var(--modal-head-line-danger); }
.modal-head.is-warn    { background: var(--modal-head-bg-warn);    border-bottom-color: var(--modal-head-line-warn); }
.modal-head.is-success { background: var(--modal-head-bg-success); border-bottom-color: var(--modal-head-line-success); }
/* Modal title sits one tier above any in-modal section titles
   (which use --fsz-h3 / 16px). Using --fsz-h2 / 18px gives the
   modal a clear "destination" feel and a parent-child rhythm
   with the sections beneath. */
.modal-title {
  font-size: var(--fsz-h2);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: var(--ls-h2);
  line-height: 1.3;
}
[dir="rtl"] .modal-title { letter-spacing: 0; }
.modal-close {
  display: inline-grid; place-items: center;
  width: 28px; height: 28px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-3);
  cursor: pointer;
  transition: all var(--t-fast);
}
.modal-close:hover { background: var(--bg-sunken); color: var(--ink); }
.modal-body {
  padding: 16px;
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px;
  /* Allow the body to shrink + scroll inside the modal's flex
     column. Without `min-height: 0` the body's intrinsic content
     height refuses to shrink past the modal's max-height, pushing
     the modal-foot below the viewport on mobile. */
  flex: 1 1 auto;
  min-height: 0;
}
.modal-foot {
  display: flex; justify-content: flex-end; align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  background: var(--bg-sunken);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
  /* Pin to the bottom of the modal flex column — never compressed
     out of view. Save/Cancel buttons must always be reachable. */
  flex-shrink: 0;
}

/* ─────────── CREATE SCHEDULE MODAL ─────────── */
/* Wider than the gen modal (more form fields) but still capped so it
   doesn't run edge-to-edge on a desktop. Body scrolls internally
   when the form's tall (custom-cadence + shared-recipients can
   stretch it past the viewport on a laptop). */
.modal--sched {
  width: 640px;
  max-width: calc(100vw - 32px);
  max-height: calc(100vh - 64px);
  display: flex;
  flex-direction: column;
}
.modal--sched .modal-body { gap: 20px; max-height: none; }
.sched-modal-body { min-height: 0; }

.sched-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.sched-section-title {
  /* Aligned with `.rpt-section-title` / `.builder-step-title` /
     `.modal-title` — schedule modal form-section dividers are at the
     same hierarchy as the parallel modules' section headers. Uses
     the `--fs-h3` shorthand (16 / 600 / ink, -0.005em via --ls-h3).
     RTL bump auto-applied via the token-level `[dir="rtl"]` redef
     in tokens.css. */
  font: var(--fs-h3);
  letter-spacing: var(--ls-h3);
  color: var(--ink);
  margin: 0;
}
.sched-section-sub {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  line-height: 1.4;
  margin-top: -4px;
}
[dir="rtl"] .sched-section-sub { font-size: var(--fsz-label); }

/* Source provenance hint that surfaces under the picker once a
   source is selected. Format: icon · type · owner · created timestamp.
   Reads as a single inline trail so the user gets the full context
   on one row without breaking the modal's vertical rhythm. The
   parts wrap to a 2nd row only on very narrow viewports. */
.sched-source-hint {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 8px;
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  padding: 8px 12px;
  background: var(--bg-sunken);
  border-radius: var(--r-sm);
}
.sched-source-hint > svg { color: var(--ink-2); flex-shrink: 0; }
.sched-source-sep {
  color: var(--ink-4);
  user-select: none;
}
.sched-source-part {
  white-space: nowrap;
}
.sched-source-part.is-kind  { color: var(--ink); font-weight: 600; }
.sched-source-part.is-owner { color: var(--ink-2); font-weight: 500; }
.sched-source-part.is-created,
.sched-source-part.is-builtin { color: var(--ink-3); font-weight: 500; }

.sched-recipients-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 4px;
}
/* Recipient dropdowns inside the schedule modal carry user names +
   email/role subtitles; bump the menu width so a "Mariam Al Tayer
   · mariam@ive.ae" row reads on a single line instead of ellipsing
   in the middle of the email. Scoped to this surface so the rest
   of the app's filter-menus keep their original 200/280 sizing. */
.sched-recipients-field .filter-menu {
  min-width: 300px;
  max-width: 420px;
}
/* Trigger button (pill) takes the full field width here so the
   "Pick users…" / "Pick roles…" target is a full-row click target
   instead of a tight content-sized pill. The caret stays pinned
   to the row's end via `justify-content: space-between`. */
.sched-recipients-field .filter-pill-wrap {
  display: block;
  width: 100%;
}
.sched-recipients-field .filter-pill {
  width: 100%;
  justify-content: space-between;
}

@media (max-width: 720px) {
  /* dvh (dynamic viewport) replaces vh so the modal respects the
     visible viewport when iOS Safari shows the URL bar — otherwise
     the modal's vh-based height makes the foot drop below the
     fold and Save/Cancel become unreachable. */
  .modal--sched { width: 100%; max-width: 100%; max-height: 100dvh; border-radius: 0; }
}

/* ─────────── SAVE TEMPLATE CONFIRM MODAL ─────────── */
.modal--confirm {
  width: 560px;
  max-width: calc(100vw - 32px);
  max-height: calc(100vh - 64px);
  display: flex;
  flex-direction: column;
}
.modal--confirm .modal-body { gap: 16px; }
.confirm-body { min-height: 0; }

.confirm-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.confirm-section-title {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  text-transform: uppercase;
}
[dir="rtl"] .confirm-section-title { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }

/* Definition-list block for the template summary — `dt` is the
   field name (muted), `dd` is the value (regular ink). Two columns
   so the values align vertically across rows. */
.confirm-rows {
  display: grid;
  grid-template-columns: 110px 1fr;
  gap: 6px 12px;
  margin: 0;
  padding: 12px 14px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.confirm-row { display: contents; }
.confirm-row dt {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-weight: 500;
  margin: 0;
}
.confirm-row dd {
  font-size: var(--fsz-body);
  color: var(--ink);
  margin: 0;
  word-break: break-word;
}
.confirm-row-strong { font-weight: 600; }

.confirm-sharing-head {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.confirm-reach {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  font-weight: 500;
}

/* Visibility pill in confirm modals — larger and more substantive
   than the tiny `.tpl-shared` row pill. Carries the visibility name
   and the total-reach count on a 2-line stack so the user reads the
   intended scope at a glance. Tinted background per tone, matching
   the tone semantics elsewhere (private = neutral, public = brand,
   shared = brand-light). */
.confirm-vis-pill {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--bg-raised);
  align-self: flex-start;
  min-width: 220px;
}
.confirm-vis-pill-icon {
  width: 32px; height: 32px;
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
  background: var(--bg-sunken);
  color: var(--ink-3);
  flex-shrink: 0;
}
.confirm-vis-pill-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.confirm-vis-pill-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
}
[dir="rtl"] .confirm-vis-pill-name { letter-spacing: 0; }
.confirm-vis-pill-sub {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-3);
}
[dir="rtl"] .confirm-vis-pill-sub { font-size: var(--fsz-label); }
/* Tone tints */
.confirm-vis-pill.is-public {
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand-500) 28%, var(--line));
}
.confirm-vis-pill.is-public .confirm-vis-pill-icon {
  background: var(--bg-raised);
  color: var(--brand-700);
}
.confirm-vis-pill.is-public .confirm-vis-pill-name { color: var(--brand-700); }
.confirm-vis-pill.is-shared {
  background: color-mix(in oklab, var(--brand-500) 6%, var(--bg-raised));
  border-color: color-mix(in oklab, var(--brand-500) 20%, var(--line));
}
.confirm-vis-pill.is-shared .confirm-vis-pill-icon {
  background: var(--brand-tint);
  color: var(--brand-700);
}

.confirm-recipient-list {
  list-style: none;
  margin: 0;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  max-height: 220px;
  overflow-y: auto;
}
.confirm-recipient {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.confirm-recipient-empty {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  text-align: center;
  padding: 12px;
}

/* Quiet info banner used for Public / Private notes — sets context
   on the share without yelling. */
.confirm-callout {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  background: color-mix(in oklab, var(--brand-500) 6%, var(--bg-sunken));
  border: 1px solid color-mix(in oklab, var(--brand-500) 18%, var(--line));
  border-radius: var(--r-md);
  padding: 10px 12px;
  line-height: 1.4;
}

@media (max-width: 720px) {
  /* dvh: see .modal--sched note above. */
  .modal--confirm { width: 100%; max-width: 100%; max-height: 100dvh; border-radius: 0; }
  .confirm-rows { grid-template-columns: 1fr; gap: 2px; }
  .confirm-row dt { margin-top: 6px; }
}

/* Small status-action confirm modal — narrower than the heavy
   save-template/save-schedule confirms; just title + prose body
   + Cancel/Confirm. Used by the row Activate / Deactivate buttons. */
.modal--action-confirm {
  width: 460px;
  max-width: calc(100vw - 32px);
}
.confirm-action-body {
  font-size: var(--fsz-body);
  color: var(--ink-2);
  line-height: 1.5;
}
.confirm-action-body p {
  margin: 0 0 8px;
}
.confirm-action-body p:last-child {
  margin-bottom: 0;
}
.confirm-action-body strong {
  color: var(--ink);
  font-weight: 600;
}

/* ─────────── SCHEDULE HISTORY MODAL ─────────── */
/* Source line above the history list — surfaces which schedule's
   runs the user is looking at. Reuses the modal-body padding from
   the schedule modal. */
.history-source-line {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: var(--fsz-label);
  color: var(--ink-3);
  margin-bottom: 4px;
}
.history-source-label {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-4);
}
[dir="rtl"] .history-source-label { font-size: var(--fsz-caption); letter-spacing: 0; text-transform: none; }
.history-source-name {
  font-weight: 600;
  color: var(--ink);
}

.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.history-row {
  display: grid;
  grid-template-columns: 28px 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 10px 12px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.history-row.is-fail {
  background: color-mix(in oklab, var(--err) 6%, var(--bg-sunken));
  border-color: color-mix(in oklab, var(--err) 25%, var(--line));
}
.history-row.is-skipped {
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 8%, var(--bg-sunken));
  border-color: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 28%, var(--line));
}
.history-mark {
  width: 28px; height: 28px;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  background: var(--bg-raised);
  color: var(--ink-3);
  border: 1px solid var(--line);
}
.history-mark.is-ok      { background: color-mix(in oklab, var(--ok)  16%, var(--bg-raised)); color: var(--ok);  border-color: color-mix(in oklab, var(--ok) 30%, var(--line)); }
.history-mark.is-fail    { background: color-mix(in oklab, var(--err) 16%, var(--bg-raised)); color: var(--err); border-color: color-mix(in oklab, var(--err) 35%, var(--line)); }
.history-mark.is-skipped { background: var(--bg-raised); color: oklch(0.5 0.13 75); border-color: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 35%, var(--line)); }
.history-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.history-date-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.history-date {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
}
.history-time {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}
.history-meta {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}
[dir="rtl"] .history-meta { font-size: var(--fsz-label); }
.history-duration {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.history-empty {
  font-size: var(--fsz-body);
  color: var(--ink-3);
  text-align: center;
  padding: 24px 16px;
}

/* Inline duplicate-conflict banner inside the duplicate modal.
   Flags that the source's exact configuration is already saved as
   another template — uses the warn ramp (amber) since this is a
   blocked state, not a destructive one (no data is at risk). */
.dup-conflict {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  margin: 10px 0;
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 10%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 36%, var(--line));
  border-radius: var(--r-md);
}
.dup-conflict-icon {
  width: 22px; height: 22px;
  flex-shrink: 0;
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in oklab, var(--warn, oklch(0.72 0.15 75)) 28%, var(--bg-raised));
  color: oklch(0.5 0.13 75);
}
.dup-conflict-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.dup-conflict-title {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: oklch(0.42 0.13 75);
}
[dir="rtl"] .dup-conflict-title { font-size: var(--fsz-body); }
.dup-conflict-body {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.4;
}
[dir="rtl"] .dup-conflict-body { font-size: var(--fsz-label); }
@media (max-width: 720px) {
  .modal--action-confirm { width: 100%; max-width: 100%; }
}

/* Email-source highlight in the schedule confirm — set expectation
   that deliveries arrive from a dedicated noreply mailbox so users
   know what address recipients should look for / safe-list. */
.confirm-email-card {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  background: color-mix(in oklab, var(--brand-500) 5%, var(--bg-sunken));
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  border-radius: var(--r-md);
}
.confirm-email-icon {
  width: 28px; height: 28px;
  flex-shrink: 0;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  background: var(--brand-tint);
  color: var(--brand-700);
  border: 1px solid color-mix(in oklab, var(--brand-500) 30%, var(--line));
}
.confirm-email-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.confirm-email-headline {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
}
[dir="rtl"] .confirm-email-headline { font-size: var(--fsz-body); }
.confirm-email-body {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.45;
}
[dir="rtl"] .confirm-email-body { font-size: var(--fsz-label); }
.confirm-email-from {
  display: inline-block;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--brand-700);
  background: var(--bg-raised);
  border: 1px solid color-mix(in oklab, var(--brand-500) 30%, var(--line));
  border-radius: var(--r-sm);
  padding: 1px 6px;
  margin: 0 2px;
  white-space: nowrap;
}
.confirm-email-tip {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-style: italic;
}
[dir="rtl"] .confirm-email-tip { font-size: var(--fsz-label); font-style: normal; }

/* Confirm-phase summary block — what's about to be generated. */
.gen-summary {
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
  display: flex; flex-direction: column; gap: 8px;
}
.gen-summary-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
}
[dir="rtl"] .gen-summary-label { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
/* Stacked label / value rows. Each row is its own line so a long scope trail
   (e.g. "Northline Academy › Al Barsha › 1668593044 · Ahmed Al Hashimi") can
   wrap cleanly without competing with neighbouring fields for width. */
.gen-summary-rows {
  display: flex;
  flex-direction: column;
  margin: 0;
}
.gen-summary-row {
  display: grid;
  grid-template-columns: 88px 1fr;
  align-items: baseline;
  gap: 12px;
  padding: 7px 0;
  font-size: var(--fsz-body);
  border-top: 1px solid color-mix(in oklab, var(--ink) 6%, transparent);
}
.gen-summary-row:first-child { border-top: 0; padding-top: 4px; }
.gen-summary-row dt {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
  white-space: nowrap;
}
[dir="rtl"] .gen-summary-row dt { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.gen-summary-row dd {
  margin: 0;
  color: var(--ink);
  font-weight: 400;
  word-break: break-word;
  min-width: 0;
}
/* Report-ID and other identifier values render in JetBrains Mono so
   they read as machine-generated codes rather than prose. The class
   is named `.mono` for back-compat; here we override the global
   `.mono` rule (which only sets tabular-nums on Inter Tight) with
   the canonical `--fs-code` token. */
.gen-summary-row dd.mono {
  font: var(--fs-code);
  letter-spacing: var(--ls-code);
  color: var(--ink);
}
@media (max-width: 480px) {
  .gen-summary-row {
    grid-template-columns: 1fr;
    gap: 2px;
  }
}

/* Scope trail — chevron-separated breadcrumb of the hierarchy this export
   covers. Direction-aware: the chevron flips for RTL via .gen-scope-sep. */
.gen-scope-trail {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 6px;
  font-weight: 500;
}
.gen-scope-seg {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 2px 8px;
  font-size: var(--fsz-label);
  color: var(--ink);
  white-space: nowrap;
}
.gen-scope-sep {
  color: var(--ink-4);
  font-size: var(--fsz-h3);
  line-height: 1;
}
[dir="rtl"] .gen-scope-sep { transform: scaleX(-1); }
.gen-scope-empty {
  color: var(--ink-3);
  font-style: italic;
  font-weight: 400;
}

/* Applied-filters list inside the modal. Each filter dimension is its own
   group on its own line: "Vehicle: 1A 2A". The chips are smaller than the
   scope-trail chips because there can be many of them; line wrapping is
   allowed so a many-value filter doesn't push the modal wider. */
.gen-filter-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.gen-filter-group {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  font-size: var(--fsz-label);
}
.gen-filter-key {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
  flex-shrink: 0;
}
[dir="rtl"] .gen-filter-key { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.gen-filter-vals {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
}
.gen-filter-chip {
  background: var(--brand-tint);
  color: var(--brand-700);
  border: 1px solid color-mix(in oklch, var(--brand-500) 20%, var(--line));
  border-radius: var(--r-sm);
  padding: 1px 7px;
  font-size: var(--fsz-label);
  font-weight: 500;
  white-space: nowrap;
}
[data-theme="dark"] .gen-filter-chip { color: var(--brand-700); }

/* Format radio cards — bigger hit-target than a default radio. */
.gen-format-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-4);
  text-transform: uppercase;
  margin-bottom: 8px;
}
[dir="rtl"] .gen-format-label { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.gen-format-options { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.gen-format-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 12px;
  border: 1.5px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: all var(--t-fast);
  background: var(--bg-raised);
}
.gen-format-opt:hover { border-color: var(--line-strong); }
.gen-format-opt.is-on {
  border-color: var(--brand-500);
  background: var(--brand-tint);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-500) 15%, transparent);
}
.gen-format-opt input { position: absolute; opacity: 0; pointer-events: none; }
.gen-format-opt svg { color: var(--ink-3); flex-shrink: 0; }
.gen-format-opt.is-on svg { color: var(--brand-700); }
.gen-format-name { font-weight: 600; color: var(--ink); font-size: var(--fsz-body); }
.gen-format-hint { font-size: var(--fsz-caption); font-weight: 500; color: var(--ink-3); margin-inline-start: auto; }
@media (max-width: 480px) {
  .gen-format-options { grid-template-columns: 1fr; }
  .gen-format-hint { margin-inline-start: 0; }
}

/* Generating-phase progress bar. */
.gen-progress { gap: 8px; }
.gen-progress-bar {
  height: 8px;
  background: var(--bg-sunken);
  border-radius: 4px;
  overflow: hidden;
}
.gen-progress-fill {
  height: 100%;
  background: var(--brand-600);
  border-radius: 4px;
  transition: width 160ms ease;
}
.gen-progress-meta {
  display: flex; justify-content: space-between;
  font-size: var(--fsz-label);
  color: var(--ink-3);
}
/* Smoother fill transition that matches the new ~100ms tick rate
   without lagging visibly behind the percent counter. */
.gen-progress-fill { transition: width 110ms ease-out; }

/* Filename being produced — sits below the progress bar so the user
   can read what's being made. The file-format icon (filePdf / fileExcel)
   reinforces the format choice. */
.gen-progress-file {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: var(--fsz-label);
  color: var(--ink-2);
  margin-top: 4px;
}
.gen-progress-file svg { color: var(--ink-3); flex-shrink: 0; }
/* The filename is a code-style identifier (path/extension) and
   should render in JetBrains Mono via --fs-code, not in Inter
   Tight (which is what the global `.mono` utility resolves to —
   it's tabular-nums-only, despite the name). Mirrors the fix on
   `.gen-summary-row dd.mono` (Report ID) for consistency. */
.gen-progress-file .mono {
  font: var(--fs-code);
  letter-spacing: var(--ls-code);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Notify-when-ready opt-in. Inline checkbox + two-line description so
   the affordance is discoverable without dominating the loader. When
   checked, the modal's Cancel button label flips to "Run in
   background" so the user has a clear escape hatch for long
   generations. */
.gen-notify {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--bg-raised);
  margin-top: 8px;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.gen-notify:hover { border-color: var(--line-strong); }
.gen-notify input[type="checkbox"] {
  width: 16px; height: 16px;
  margin-top: 2px;
  accent-color: var(--brand-600);
  flex-shrink: 0;
  cursor: pointer;
}
.gen-notify-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.gen-notify-title {
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink);
}
[dir="rtl"] .gen-notify-title { font-size: var(--fsz-body); }
.gen-notify-hint {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  line-height: 1.4;
}
[dir="rtl"] .gen-notify-hint { font-size: var(--fsz-label); }

/* Ready-phase confirmation. */
.gen-ready {
  flex-direction: row;
  align-items: center;
  gap: 14px;
  padding: 18px 16px;
}
.gen-ready-icon {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: color-mix(in oklab, var(--ok) 15%, var(--bg-raised));
  color: var(--ok);
  flex-shrink: 0;
}
.gen-ready-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.gen-ready-title { font-weight: 600; color: var(--ink); font-size: var(--fsz-h3); }
.gen-ready-file { font-size: var(--fsz-label); color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ════════════════════════════════════════════════════════════════
   DASHBOARDS MODULE
   ════════════════════════════════════════════════════════════════ */

.main.main--dash {
  /* Top padding aligned to `.main` (20px) so the first content
     line on dashboards starts at the same vertical position as
     on Saved Tests / Reports / other token-based pages. */
  padding: 20px 24px 48px;
  display: flex; flex-direction: column;
  gap: 18px;
}
/* Padding stays stable at 24px horizontal down to 720px (no 1100px
   ramp) so the page header doesn't visibly jump up + left when
   crossing the 1100px boundary. Below 720px we tighten to phone-
   appropriate spacing. */
@media (max-width: 720px)  { .main.main--dash { padding: 12px 12px 32px; gap: 14px; } }

/* ─── Header ─────────────────────────────────────────────────── */
.dash-head { display: flex; flex-direction: column; gap: 10px; }
.dash-head-row { display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap; }
.dash-head-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 280px; }
.dash-head-title {
  margin: 0; font-size: var(--fsz-h1); font-weight: 700;
  letter-spacing: -0.01em; line-height: 1.15; color: var(--ink);
}
.dash-head-sub { font-size: var(--fsz-label); color: var(--ink-3); font-weight: 500; }
[dir="rtl"] .dash-head-sub { font-size: var(--fsz-body); }

.dash-head-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
@media (max-width: 720px) { .dash-head-tools { width: 100%; justify-content: flex-start; } }

.dash-search {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0 10px; block-size: 32px;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-radius: var(--r-md); color: var(--ink-3);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
  min-inline-size: 220px;
}
.dash-search:focus-within { border-color: var(--brand-600); box-shadow: 0 0 0 3px var(--ring); color: var(--ink); }
.dash-search-input {
  flex: 1; border: none; background: transparent; outline: none;
  font: inherit; color: var(--ink); padding: 0; font-size: var(--fsz-body);
}
[dir="rtl"] .dash-search-input { font-size: var(--fsz-body); }
.dash-search-input::placeholder { color: var(--ink-4); }
.dash-search-x {
  display: grid; place-items: center;
  inline-size: 18px; block-size: 18px;
  border: none; background: var(--bg-muted); color: var(--ink-3);
  border-radius: var(--r-pill); cursor: pointer;
}
.dash-search-x:hover { background: var(--line); color: var(--ink); }

.dash-range {
  display: inline-flex; padding: 3px;
  background: var(--bg-muted); border: 1px solid var(--line);
  border-radius: var(--r-md); gap: 2px;
}
.dash-range-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border: none; background: transparent;
  font: inherit; color: var(--ink-3); font-size: var(--fsz-label); font-weight: 500;
  border-radius: var(--r-sm); cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast); white-space: nowrap;
}
/* RTL font-size bump removed — `--fsz-label` already auto-swaps
   from 12px to 13px in RTL via the `[dir="rtl"] body` root override
   in tokens.css. The previous manual bump to `--fsz-body` (15px)
   over-scaled this segmented control vs its siblings in the same
   toolbar. Same rule applied to `.dash-view-btn` + `.map-scrub-btn`
   below. See CTA audit P2. */
.dash-range-btn:hover { color: var(--ink); }
.dash-range-btn.is-on {
  background: var(--bg-raised); color: var(--ink);
  font-weight: 600; box-shadow: var(--sh-xs);
}

.dash-view-toggle {
  display: inline-flex; border: 1px solid var(--line);
  border-radius: var(--r-md); overflow: hidden; background: var(--bg-raised);
}
.dash-view-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 6px 11px; border: none; background: transparent;
  font: inherit; color: var(--ink-3); font-size: var(--fsz-label); font-weight: 500;
  cursor: pointer; transition: color var(--t-fast), background var(--t-fast);
}
/* RTL font-size bump removed — see comment on `.dash-range-btn`. */
.dash-view-btn + .dash-view-btn { border-inline-start: 1px solid var(--line); }
.dash-view-btn:hover { color: var(--ink); background: var(--bg-muted); }
.dash-view-btn.is-on { background: var(--brand-tint); color: var(--brand-700); font-weight: 600; }

.dash-refresh {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--ink-3);
  border-radius: var(--r-md); cursor: pointer; font: inherit; font-size: var(--fsz-label);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.dash-refresh:hover { color: var(--ink); border-color: var(--line-strong); }
.dash-refresh-time { font-weight: 600; color: var(--ink); }
.dash-refresh-label { color: var(--ink-3); font-size: var(--fsz-caption); }
@media (max-width: 720px) { .dash-refresh-label { display: none; } }

.dash-live { display: inline-grid; place-items: center; }

.dash-empty {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 60px 20px; text-align: center;
  background: var(--bg-raised);
  border: 1px dashed var(--line-2);
  border-radius: var(--r-lg); color: var(--ink-3);
}
.dash-empty-title { font-size: var(--fsz-h3); font-weight: 600; color: var(--ink-2); }
.dash-empty-sub { font-size: var(--fsz-label); color: var(--ink-3); }
.dash-empty-action {
  /* sm CTA tier — 4×10 padding + r-sm radius, aligned to the
     canonical `.btn-sm`. Previously 6×12 + r-md, which was off
     the system padding scale. */
  margin-top: 6px; padding: 4px 10px;
  border: 1px solid var(--line); background: var(--bg);
  color: var(--ink); border-radius: var(--r-sm);
  font: inherit; font-size: var(--fsz-label); cursor: pointer;
}
.dash-empty-action:hover { background: var(--bg-muted); border-color: var(--line-strong); }

.dash-spark { width: 100%; display: block; }
.dash-spark-wrap { display: block; inline-size: 100%; }

.dash-stack {
  display: flex; inline-size: 100%;
  background: var(--bg-muted); border-radius: var(--r-pill); overflow: hidden;
}
.dash-stack-seg { display: block; transition: flex-basis var(--t-med); }
.dash-stack-seg.is-pass    { background: var(--ok); }
.dash-stack-seg.is-fail    { background: var(--err); }
.dash-stack-seg.is-pending { background: color-mix(in oklab, var(--brand-500) 60%, var(--ink-3)); }
.dash-stack-seg.is-absent  { background: var(--ink-4); }
.dash-stack-seg.is-warn    { background: var(--warn); }

.dash-progress { display: inline-flex; align-items: center; gap: 4px; inline-size: 100%; }
.dash-progress-track {
  flex: 1; block-size: 6px; background: var(--bg-muted);
  border-radius: var(--r-pill); overflow: hidden;
}
.dash-progress-fill {
  display: block; block-size: 100%; background: var(--brand-600);
  border-radius: inherit; transition: inline-size var(--t-med);
}
.dash-progress[data-tone="pass"] .dash-progress-fill { background: var(--ok); }
.dash-progress[data-tone="fail"] .dash-progress-fill { background: var(--err); }
/* Cyan tone — used for Contribution bars in the institutes/branches
   tables. Matches the Active Examiners KPI spark (--chart-3) and
   keeps the brand purple reserved for controls/active states rather
   than compositional data. */
.dash-progress[data-tone="cyan"] .dash-progress-fill { background: var(--chart-3); }
.dash-progress[data-tone="cyan"] .dash-progress-track {
  background: color-mix(in oklab, var(--chart-3) 14%, var(--bg-muted));
}
.dash-progress-num { font-size: var(--fsz-label); color: var(--ink); min-inline-size: 32px; text-align: end; font-variant-numeric: tabular-nums; }

.dash-delta {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 2px 7px; border-radius: var(--r-pill);
  font-size: var(--fsz-caption); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.dash-delta.is-up   { background: color-mix(in oklab, var(--ok)   18%, var(--bg)); color: var(--ok); }
.dash-delta.is-down { background: color-mix(in oklab, var(--err)  18%, var(--bg)); color: var(--err); }
.dash-delta.is-flat { background: var(--bg-muted); color: var(--ink-3); }
.dash-delta-arrow { font-weight: 700; line-height: 1; }
.dash-sev-dot.is-major { background: var(--err); }
.dash-sev-dot.is-minor { background: var(--warn); }
.org-strip-card-rate.is-pass { color: var(--ok); }
.org-strip-card-fail .tnum:first-child { color: var(--ink-2); font-weight: 600; }
.org-strip-card-fail .is-fail { color: var(--err); font-weight: 600; }

.org-delta {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: var(--r-pill);
  font-size: var(--fsz-caption); font-weight: 600; font-variant-numeric: tabular-nums;
}
.org-delta.is-up   { background: color-mix(in oklab, var(--ok)   14%, var(--bg)); color: var(--ok); }
.org-delta.is-down { background: color-mix(in oklab, var(--err)  14%, var(--bg)); color: var(--err); }
.org-delta.is-flat { background: var(--bg-muted); color: var(--ink-3); }
.org-delta-arrow { font-weight: 700; line-height: 1; }
.org-delta-label { color: var(--ink-3); font-weight: 500; margin-inline-start: 4px; text-transform: none; letter-spacing: 0; }
[dir="rtl"] .org-delta-label { font-size: var(--fsz-label); }

.org-live {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: var(--r-pill);
  background: color-mix(in oklab, var(--err) 14%, var(--bg));
  color: var(--err); font-size: var(--fsz-caption); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.org-live-dot {
  display: inline-block; inline-size: 6px; block-size: 6px;
  border-radius: 50%; background: var(--err); color: var(--err);
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--err) 50%, transparent);
  animation: pill-dot-pulse 2.4s ease-out infinite;
}

.mini-line-wrap { display: block; inline-size: 100%; block-size: 100%; position: relative; }
.mini-line { display: block; inline-size: 100%; block-size: 100%; }

.chart-tip-divider { border-block-start: 1px solid var(--tooltip-border); margin: 4px 0; }

/* ════════════════════════════════════════════════════════════════
   INSTITUTE CARD GRID
   ════════════════════════════════════════════════════════════════ */
.inst-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* ════════════════════════════════════════════════════════════════
   BRANCHES CHART VIEW (List · Chart · Map → "Chart")
   ──────────────────────────────────────────────────────────────
   Multi-style branch comparison: bar (per-branch sorted bars),
   line (smoothed curve over branches), or radar (one polygon per
   branch across all metrics). Style + metric chips controls sit
   above the canvas; the canvas itself is a fixed-height card so
   the chart size is predictable across viewports.
   ════════════════════════════════════════════════════════════════ */
.branches-chart {
  display: flex;
  flex-direction: column;
  /* Shrink-wraps the chart grid — no min-block-size now that the
     L2 map has been removed, so the section's bottom padding
     matches the top instead of trailing empty space below the
     panels. */
}
.branches-chart-canvas {
  flex: 1;
  min-block-size: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.branches-chart-canvas.is-split {
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
}

/* ── Split view ── shared row primitives + 3- and 4-panel grids ──
   Both Split layouts use the same rows: one branch per row, with
   name on the left, a chart segment in the middle, and a numeric
   value on the right. A single hover index drives row highlighting
   across every panel in the layout. */
.bx-split {
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Panel chrome — each Split panel is its own card. */
.bx-split-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px 14px;
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: 12px;
  box-shadow: 0 1px 2px -1px rgba(15, 17, 21, 0.06);
  min-inline-size: 0;
  /* Container query context — children that use cqw units size
     against the panel's width, not the viewport. So when the
     bx-chart-grid drops from 4-col to 2×2 (panels widen), bars
     scale back up automatically without manual breakpoints. */
  container-type: inline-size;
  container-name: bx-panel;
}
.bx-split-panel-head {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding-block-end: 2px;
}
/* Title row holds the panel title on one side and (optional)
   actions on the other — used by the activity panel to surface
   filter pills opposite the title without stealing rows from the
   chart body below. No min-block-size — the title text + compact
   pills are both ~18-20px tall, which is the natural row height. */
.bx-split-panel-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.bx-split-panel-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
/* Hairline divider separates the title block from the chart body
   inside each panel. 6px above the line + 4px below gives the
   title and the chart visible breathing room without the title
   feeling crammed against the rule. */
.bx-chart .bx-split-panel-head {
  border-block-end: 1px solid var(--d-line);
  padding-block-end: 6px;
  margin-block-end: 4px;
}
.bx-chart .bx-split-rows { gap: 4px; }
.bx-split-panel-title {
  font-size: var(--fsz-label);
  font-weight: 700;
  color: var(--d-ink);
  letter-spacing: 0.01em;
}
.bx-split-panel-sub {
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  letter-spacing: 0.01em;
}
.bx-split-row:hover,
.bx-split-row.is-hov {
  background: color-mix(in oklab, var(--d-indigo) 6%, transparent);
}
.bx-split-row-val.is-over { color: var(--d-ok); }
.bx-split-row-val.is-under { color: var(--d-err); }
.bx-split-seg.is-pass    { background: var(--pass); }
.bx-split-seg.is-fail    { background: color-mix(in oklab, var(--fail)    62%, var(--d-bg-card)); }
.bx-split-seg.is-absent  { background: color-mix(in oklab, var(--absent-border) 60%, var(--d-bg-card)); }
.bx-split-seg.is-pending { background: color-mix(in oklab, var(--pending) 62%, var(--d-bg-card)); }
.bx-split-row:hover .bx-split-seg.is-fail,
.bx-split-row.is-hov .bx-split-seg.is-fail    { background: var(--fail); }
.bx-split-row:hover .bx-split-seg.is-absent,
.bx-split-row.is-hov .bx-split-seg.is-absent  { background: var(--absent-border); }
.bx-split-row:hover .bx-split-seg.is-pending,
.bx-split-row.is-hov .bx-split-seg.is-pending { background: var(--pending); }
.bx-split-rate-track.is-dual {
  block-size: 16px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 2px 0;
  background: color-mix(in oklab, var(--d-ink) 5%, transparent);
  overflow: visible;
  border-radius: 8px;
}
.bx-split-rate-track.is-dual .bx-split-rate-fill {
  position: relative;
  block-size: 5px;
  inset: auto;
  margin-inline-start: 0;
}
.bx-split-rate-fill.is-pr,
.bx-split-rate-fill.is-ft {
  border-radius: 999px;
}
.bx-split-rate-track.is-dual .bx-split-rate-target {
  inset-block: -3px;
}
.bx-split-legend-sw.is-pass    { background: var(--pass); }
.bx-split-legend-sw.is-fail    { background: var(--fail); }
.bx-split-legend-sw.is-absent  { background: var(--absent-border); }
.bx-split-legend-sw.is-pending { background: var(--pending); }

/* ── Split 2 — vertical pill-stack bars panel ─────────────────── */
.bx-s2-bars-canvas {
  position: relative;
  inline-size: 100%;
  block-size: clamp(280px, 40vh, 340px);
}
.bx-s2-bars-svg {
  position: absolute; inset: 0; inline-size: 100%; block-size: 100%; display: block;
}
.bx-s2-overlay { position: absolute; inset: 0; }
.bx-s2-grid {
  stroke: var(--d-line);
  stroke-width: 1;
  stroke-dasharray: 2 5;
  opacity: 0.6;
}
.bx-s2-hov-band { fill: color-mix(in oklab, var(--d-indigo) 7%, transparent); }
.bx-s2-tick-l-col {
  position: absolute; inset-block: 0; inline-size: 50px; pointer-events: none;
  inset-inline-start: 0;
}
.bx-s2-tick-l {
  position: absolute;
  inset-inline-end: 8px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  transform: translateY(-50%);
  text-align: end;
  white-space: nowrap;
}
.bx-s2-col {
  position: absolute;
  inset-block: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
}
.bx-s2-track {
  position: absolute;
  inline-size: clamp(18px, 50%, 32px);
  inset-inline-start: 50%;
  transform: translateX(-50%);
  background: color-mix(in oklab, var(--d-ink) 6%, transparent);
  border-radius: 999px;
  padding: 4px 0;
  pointer-events: auto;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset;
  overflow: visible;
}
.bx-s2-pills {
  inline-size: 100%;
  block-size: 100%;
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: 3px;
  padding-inline: 3px;
}
.bx-s2-pill {
  inline-size: 100%;
  display: block;
  border-radius: 999px;
  min-block-size: 4px;
  transition: background-color 160ms ease;
}
.bx-s2-pill.is-pass    { background: var(--pass); box-shadow: 0 0 0 1px color-mix(in oklab, var(--pass) 30%, transparent) inset; }
.bx-s2-pill.is-fail    { background: color-mix(in oklab, var(--fail)    24%, transparent); }
.bx-s2-pill.is-absent  { background: color-mix(in oklab, var(--absent-border) 30%, transparent); }
.bx-s2-pill.is-pending { background: color-mix(in oklab, var(--pending) 24%, transparent); }
.bx-s2-col.is-hov .bx-s2-track {
  background: color-mix(in oklab, var(--d-ink) 9%, transparent);
}
.bx-s2-col.is-hov .bx-s2-pill.is-fail    { background: var(--fail); }
.bx-s2-col.is-hov .bx-s2-pill.is-absent  { background: var(--absent-border); }
.bx-s2-col.is-hov .bx-s2-pill.is-pending { background: var(--pending); }
.bx-s2-hq {
  position: absolute;
  inset-block-start: -18px;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  font-size: var(--fsz-chart-label);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--d-purple);
  background: color-mix(in oklab, var(--d-purple) 16%, transparent);
  padding: 1px 5px;
  border-radius: 3px;
  white-space: nowrap;
}
.bx-s2-xlabel {
  position: absolute;
  inset-block-end: -22px;
  inline-size: 100%;
  text-align: center;
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--d-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding-inline: 4px;
}

/* ── Radar (Split 2 right panel) ──────────────────────────────── */
.bx-radar {
  inline-size: 100%;
  block-size: clamp(260px, 40vh, 320px);
  display: block;
}
.bx-radar-ring  { fill: none; stroke: var(--d-line-soft); stroke-width: 1; }
.bx-radar-spoke { stroke: var(--d-line-soft); stroke-width: 1; }
.bx-radar-axis  { font-size: var(--fsz-chart-label); fill: var(--d-ink-3); font-weight: 600; }
.bx-radar-legend {
  list-style: none;
  margin: 6px 0 0 0;
  padding: 8px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  border-block-start: 1px solid var(--d-line-soft);
}
.bx-radar-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-2);
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 4px;
  transition: background-color 120ms ease;
}
.bx-radar-legend-item:hover,
.bx-radar-legend-item.is-hov {
  background: color-mix(in oklab, var(--d-indigo) 8%, transparent);
  color: var(--d-ink);
}
.bx-radar-legend-dot { inline-size: 8px; block-size: 8px; border-radius: 50%; flex-shrink: 0; }
.bx-radar-legend-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 120px; }
.bx-radar-legend-hq {
  font-size: var(--fsz-chart-label); font-weight: 700; color: var(--d-purple);
  background: color-mix(in oklab, var(--d-purple) 14%, transparent);
  padding: 1px 5px; border-radius: 3px;
}

/* ── Split 4 — Bullet chart (Pass Rate) ───────────────────────── */
.bx-bullet-list {
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.bx-bullet-row {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 3fr) minmax(50px, auto);
  align-items: center;
  gap: 10px;
  padding: 4px 6px;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 120ms ease;
}
.bx-bullet-row:hover,
.bx-bullet-row.is-hov {
  background: color-mix(in oklab, var(--d-indigo) 6%, transparent);
}
.bx-bullet-name {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fsz-label); font-weight: 600; color: var(--d-ink); min-inline-size: 0;
}
.bx-bullet-name-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bx-bullet-hq {
  font-size: var(--fsz-chart-label); font-weight: 700; color: var(--d-purple);
  background: color-mix(in oklab, var(--d-purple) 14%, transparent);
  padding: 1px 5px; border-radius: 3px;
}
.bx-bullet-track {
  position: relative;
  block-size: 14px;
  border-radius: 999px;
  overflow: hidden;
  display: flex;
}
.bx-bullet-band       { display: block; block-size: 100%; }
.bx-bullet-band.is-band-low  { background: color-mix(in oklab, var(--d-err)  18%, transparent); }
.bx-bullet-band.is-band-mid  { background: color-mix(in oklab, var(--d-warn) 22%, transparent); }
.bx-bullet-band.is-band-high { background: color-mix(in oklab, var(--d-ok)   22%, transparent); }
.bx-bullet-fill {
  position: absolute;
  inset-block: 3px;
  inset-inline-start: 0;
  block-size: 8px;
  border-radius: 999px;
  transition: inline-size 200ms ease, background-color 160ms ease;
}
.bx-bullet-fill.is-over  { background: var(--d-ok); }
.bx-bullet-fill.is-under { background: var(--d-err); }
.bx-bullet-target {
  position: absolute;
  inset-block: -2px;
  inline-size: 2px;
  background: var(--d-ink);
  transform: translateX(-50%);
  z-index: 2;
  border-radius: 1px;
  pointer-events: none;
}
.bx-bullet-value {
  font-size: var(--fsz-caption); font-weight: 600; color: var(--d-ink); text-align: end; white-space: nowrap;
}

/* ── Split 4 — Gauge grid (1st-Time Pass) ─────────────────────── */
.bx-gauge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px;
}
.bx-gauge-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 8px 6px;
  border-radius: 10px;
  cursor: pointer;
  transition: background-color 120ms ease, transform 120ms ease;
}
.bx-gauge-card:hover,
.bx-gauge-card.is-hov {
  background: color-mix(in oklab, var(--d-indigo) 7%, transparent);
  transform: translateY(-1px);
}
.bx-gauge-svg {
  inline-size: 100%;
  max-inline-size: 110px;
  aspect-ratio: 100 / 60;
  display: block;
}
.bx-gauge-bg   { fill: none; stroke: color-mix(in oklab, var(--d-ink) 8%, transparent); stroke-width: 9; stroke-linecap: round; }
.bx-gauge-fill { fill: none; stroke-width: 9; stroke-linecap: round; transition: stroke-dasharray 200ms ease; }
.bx-gauge-target { stroke: var(--d-ink); stroke-width: 2; }
.bx-gauge-num  { font-size: var(--fsz-h3); font-weight: 700; fill: var(--d-ink); font-variant-numeric: tabular-nums; }
.bx-gauge-name {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fsz-caption); font-weight: 600; color: var(--d-ink);
  inline-size: 100%; min-inline-size: 0;
}
.bx-gauge-name-text {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; text-align: center;
}
.bx-gauge-hq {
  font-size: var(--fsz-chart-label); font-weight: 700; color: var(--d-purple);
  background: color-mix(in oklab, var(--d-purple) 14%, transparent);
  padding: 1px 4px; border-radius: 3px;
}

/* ── Draft layout ── 4 panels (arc / pie / outcomes / radar) +
   one shared branch legend at the top. Each branch's identity is
   carried by color throughout — never repeated as text inside
   panels. ── */
.bx-chart {
  /* Sized by its own content (legend + chart-grid) so the section
     wrapping it shrinks to fit. flex:1 used to stretch the chart
     to fill its parent, leaving empty space below the grid. */
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-block-size: 0;
}
.bx-chart-legend {
  /* Chips sit directly in the section flow — no card chrome around
     them. The chips themselves carry their own backgrounds/borders
     so the legend reads as a row of pills, not a panel. */
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.bx-chart-chip {
  --chip-color: var(--d-indigo);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px 5px 8px;
  background: transparent;
  border: 1px solid var(--d-line);
  border-radius: 999px;
  cursor: pointer;
  color: var(--d-ink-2);
  font-size: var(--fsz-label);
  font-weight: 500;
  /* Anchor-specific resets — chip is now an <a> that navigates to
     the branch dashboard. Drop the default link underline + colour. */
  text-decoration: none;
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease, opacity 140ms ease;
}
.bx-chart-chip:hover,
.bx-chart-chip.is-hov {
  border-color: color-mix(in oklab, var(--chip-color) 60%, var(--d-line));
  color: var(--d-ink);
  background: color-mix(in oklab, var(--chip-color) 8%, transparent);
}
.bx-chart-chip:focus-visible {
  outline: 2px solid var(--d-indigo, var(--brand-500));
  outline-offset: 2px;
}
.bx-chart-chip.is-dim {
  opacity: 0.4;
}
.bx-chart-chip-dot {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 50%;
  background: var(--chip-color);
  flex-shrink: 0;
}
.bx-chart-chip-name {
  white-space: nowrap;
}
.bx-chart-chip-hq {
  font-size: var(--fsz-chart-label);
  font-weight: 700;
  color: var(--d-purple);
  background: color-mix(in oklab, var(--d-purple) 16%, transparent);
  padding: 1px 5px;
  border-radius: 3px;
}
/* `.bx-chart-clear` removed — was the "clear pin" button shown
   when click-to-pin was active. Click now navigates to the branch
   dashboard, so there is no pin state to clear. */

/* Branch-chip drill-down hint — migrated to the unified ChartTip
   primitive. The chip-hover delay (300ms) lives in dashboard-views
   alongside the hover state; the floating chrome (inverted bg,
   arrow, auto-placement, portal) comes from `.chart-tip`. */

/* 1×3 grid for the 3 panels at desktop (Test outcomes ·
   1st-Time Pass · Contribution); collapses to a 3×1 vertical
   stack at ≤ 1100px so each chart gets the full row width
   instead of cramming into a side-by-side cell that's too
   narrow to read on tablet/mobile.
   grid-auto-rows is a FIXED 320px so each panel is the same
   height at every viewport — desktop 1×3 produces one row of
   320px; narrow 3×1 produces three rows of 320px and the
   container grows to fit. */
.bx-chart-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  min-block-size: 0;
}
.bx-chart-grid > .bx-split-panel {
  min-block-size: 0;
  overflow: hidden;
  /* Top padding clears the panel title + subtitle; bottom padding
     gives the chart canvas breathing room above the panel border.
     Sides match the desktop card chrome. */
  padding: 14px 14px 16px;
  /* In the 4-up zone the panels keep a roughly square aspect so
     when the viewport shrinks toward the 1100px breakpoint the
     PANEL HEIGHT shrinks alongside its width — keeps the panels
     from becoming awkwardly tall + narrow. The 2×2 / 1-col
     breakpoints below override this to a fixed-row layout where
     each panel is wider than it is tall. */
  aspect-ratio: 1 / 1;
}
.bx-chart-grid > .bx-split-panel > .bx-split-rows {
  flex: 1;
  min-block-size: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* 2×2 grid at the d2-shell collapse boundary; full 1-col stack below
   720 (phone). Drop the aspect-ratio in these zones — the panel is
   wider than tall here so we go back to a fixed 280px row. */
@media (max-width: 1100px) {
  .bx-chart-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 280px;
  }
  .bx-chart-grid > .bx-split-panel { aspect-ratio: auto; }
}
@media (max-width: 720px) {
  .bx-chart-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ── Test-outcomes panel: vertical-bar columns ─────────────────
   One column per branch with stacked Pass / Fail / Absent /
   Pending segments anchored at the baseline. Width auto-fits to
   the panel; columns share equal flex space. */
.bx-chart-cols {
  flex: 1;
  min-block-size: 0;
  display: flex;
  align-items: stretch;
  /* Top padding scales with panel width (cqw) so at narrow 4-up
     panels (~168px) the bars get more vertical room. Range stays
     visually balanced at typical desktop. */
  padding: clamp(4px, 5cqw, 14px) 4px 0;
}
.bx-chart-col {
  flex: 1 1 0;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Gap between val / track / dot. Wider clamp (was 1.6cqw → 2.5cqw,
     ceiling 6 → 10) so % sits clearly clear of the bar top instead
     of brushing it. */
  gap: clamp(3px, 2.5cqw, 10px);
  cursor: pointer;
  position: relative;
  transition: opacity var(--t-fast);
}
.bx-chart-col.is-dim { opacity: 0.32; }
/* Single-branch variant (T3 branch dashboard, or a T2 institute with
   only one branch). Suppress the per-panel ActiveBranchBadge — it
   would just repeat the same branch name four times across the
   chart, redundant with the section heading and dash-head title. */
.bx-chart.is-single .bx-chart-panel-badge { display: none; }
.bx-chart-col-val {
  /* Clamp shrinks the % above bars at narrow viewports so adjacent
     columns don't visually crash into each other. Floor set to the
     chart-label tier (10px) so the chart stays consistent with the
     rest of the data-viz exception tokens. */
  font-size: clamp(var(--fsz-chart-label), 0.7vw, 11px);
  font-weight: 600;
  color: var(--ink-2, var(--ink));
  letter-spacing: -0.01em;
  line-height: 1;
  /* Bottom margin pushes the % off the bar top — bumped from
     clamp(2,1.5cqw,6) so the value reads as clearly separated
     from the bar at typical desktop, without crowding adjacent
     columns at narrow widths. */
  margin-block-end: clamp(4px, 2.5cqw, 10px);
}
.bx-chart-col-track {
  flex: 1;
  inline-size: 100%;
  min-block-size: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  position: relative;
}
/* Vertical equivalent of the list-view RateBarCell — each segment
   is its own pill, separated by a 2px gap; colours match the
   list view's resting (-border) and hover (full saturation)
   tokens so the two views read as the same visual language. */
.bx-chart-col-bar {
  inline-size: 100%;
  /* Continuous shrink based on the PANEL's width (cqw). Floor 8px
     so the bar can shrink at narrow 4-up panels (~168px) without
     hogging the column; ceiling 20px so wider 2×2 panels still
     get a proper-thickness bar. */
  max-inline-size: clamp(8px, 4cqw, 20px);
  block-size: 100%;
  display: flex;
  flex-direction: column-reverse;
  gap: 2px;
  background: transparent;
  border: 0;
  overflow: visible;
}
.bx-chart-cols { gap: clamp(3px, 1.5cqw, 6px); }
.bx-chart-cseg {
  display: block;
  inline-size: 100%;
  flex-shrink: 0;
  border-radius: var(--r-pill);
  transition: block-size 280ms ease, background 180ms ease;
}
.bx-chart-cseg.is-pass    { background: var(--pass); }
.bx-chart-cseg.is-fail    { background: var(--fail-border); }
.bx-chart-cseg.is-absent  { background: var(--absent-border); }
.bx-chart-cseg.is-pending { background: var(--pending-border); }
/* Reveal full-saturation colours on row hover or keyboard focus —
   matches the .rate-cell hover treatment in the list view. */
.bx-chart-col.is-hov .bx-chart-cseg.is-fail,
.bx-chart-col:focus-within .bx-chart-cseg.is-fail    { background: var(--fail); }
.bx-chart-col.is-hov .bx-chart-cseg.is-absent,
.bx-chart-col:focus-within .bx-chart-cseg.is-absent  { background: var(--absent); }
.bx-chart-col.is-hov .bx-chart-cseg.is-pending,
.bx-chart-col:focus-within .bx-chart-cseg.is-pending { background: var(--pending); }
/* Branch color reference dot below each column — branches are
   named in the chips legend at the top of the chart, so the bar's
   only job below the column is to show the colour reference back
   to the chip. Smaller dot at narrow viewports for tighter
   density. */
.bx-chart-col-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--col-color, var(--d-ink-3));
  cursor: pointer;
}
/* Legacy .bx-chart-col-name kept for any external consumers but
   wrapped in the same display-flow it used to occupy. */
.bx-chart-col-name {
  inline-size: 100%;
  text-align: center;
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--col-color, var(--ink-2));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.15;
}
.bx-chart-col-tip {
  position: absolute;
  inset-block-end: calc(100% + 8px);
  inset-inline-start: 50%;
  transform: translateX(-50%);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 8px 10px;
  font-size: var(--fsz-label);
  min-inline-size: 220px;
  z-index: 10;
  box-shadow: 0 6px 22px -10px rgba(15, 17, 21, 0.18);
  pointer-events: none;
}
/* Floating variant — when the tip uses position:fixed coords, the
   inline style overrides position/left/bottom; this rule clears
   the base rule's logical-inset anchors and bumps z-index above
   the dashboard chrome so it stays visible across scroll. */
.bx-chart-col-tip.is-floating {
  inset-block-end: auto;
  inset-inline-start: auto;
  inset-block-start: auto;
  inset-inline-end: auto;
  z-index: var(--z-tooltip);
}

/* ── Tests-completed trend panel (single-line spark) ─────────
   Mirrors the T1 KpiBigArea spark style: gradient fill, smooth
   Catmull-Rom line, dashed crosshair on hover, single dot at
   hovered point, no gridlines (the SplitPanel chrome handles
   the framing). X-axis labels render as HTML so they don't
   stretch with the SVG's preserveAspectRatio="none". */
.bx-trend-wrap {
  flex: 1;
  min-block-size: 0;
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  position: relative;
  cursor: crosshair;
}
.bx-trend-svg {
  flex: 1;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
  display: block;
}
/* In RTL the axis labels (`inset-inline-start`) and the SVG drawing
   (LTR-by-default) used to disagree — the line would peak on the
   visual LEFT while its label sat on the visual RIGHT. Mirror the
   SVG so xs[0] visually anchors to the right edge (matching the
   axis label flip). The hover-idx calc inverts ratio in RTL to keep
   the cursor over the bucket the user is reading. */
[dir="rtl"] .bx-trend-svg { transform: scaleX(-1); }
.bx-trend-stage {
  position: relative;
  flex: 1;
  inline-size: 100%;
  min-block-size: 0;
  display: flex;
}
.bx-trend-hover-dot {
  position: absolute;
  transform: translate(-50%, -50%);
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--brand-500);
  border: 1.4px solid var(--d-bg-card);
  box-shadow: 0 1px 2px oklch(0 0 0 / 0.15);
  pointer-events: none;
  z-index: 1;
}
/* In RTL `inset-inline-start: X%` resolves to `right: X%`, anchoring
   the dot's RIGHT edge. `translate(-50%, -50%)` always shifts in
   screen-LEFT regardless of direction, so the dot ends up offset
   by its own width in RTL. Flip the X axis of the centering
   translate to compensate. */
[dir="rtl"] .bx-trend-hover-dot { transform: translate(50%, -50%); }
.bx-trend-axis-row {
  position: relative;
  inline-size: 100%;
  block-size: 14px;
  margin-block-start: 4px;
  pointer-events: none;
}
.bx-trend-axis-tick {
  position: absolute;
  transform: translateX(-50%);
  /* Continuous shrink so labels don't crowd at narrow viewports
     while staying readable at desktop. */
  font-size: var(--fsz-chart-label); /* was clamp(8px,0.72vw,10px) — snapped to chart-label tier */
  font-weight: 500;
  color: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.bx-trend-axis-tick.is-first { transform: translateX(0); }
.bx-trend-axis-tick.is-last  { transform: translateX(-100%); }

/* ── Panel: 1st-Time Pass concentric rings ───────────────────── */
.bx-chart-arc-wrap {
  display: flex;
  /* Pinned to bottom (was center) so the gauge sits at the base of
     its panel like the other panels — keeps single-branch arcs
     from floating awkwardly with empty space below them. */
  align-items: flex-end;
  justify-content: center;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
  position: relative;  /* anchor for the HTML center-text overlay */
}
/* Center number + label rendered as HTML over the SVG so the
   font size stays fixed at every viewport (in-SVG <text>
   shrinks with the viewBox at narrow res — became unreadable
   per user feedback). Centered over the gauge's flat bottom
   edge, mirroring the previous in-SVG position. */
.bx-chart-arc-center {
  position: absolute;
  /* Pulled up from 18% so the center number tucks just under the
     arc's lower edges (~5–6px gap) instead of floating in dead
     space below them. Going higher overlaps the outermost ring. */
  inset-block-end: 23%;
  /* Physical `left: 50%` + `translateX(-50%)` so the centre stays
     under the arc in both LTR and RTL — the SVG geometry is
     direction-agnostic. */
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  pointer-events: none;
}
/* Stage locks the arc's aspect ratio (380:240 — matches the SVG
   viewBox) so the HTML label overlay's percent-based positions
   coincide with the SVG content. Without this the SVG letterboxes
   inside .bx-chart-arc-wrap (default preserveAspectRatio="meet")
   and the labels drift off the rendered arc. */
.bx-chart-arc-stage {
  position: relative;
  inline-size: 100%;
  /* Cap chosen so the SVG scales up to ~0.76× at most, keeping the
     arc's rendered stroke width inside the same 16–20 CSS-px band
     the Test Outcomes bars use rather than ballooning at wide
     panels. */
  max-inline-size: 290px;
  /* Cap height to wrap so the aspect-ratio doesn't push the stage
     past the wrap (overflow:hidden on the panel was clipping the
     50 / 0 / 100 labels at the top at narrow 4-up panels). */
  max-block-size: 100%;
  aspect-ratio: 380 / 240;
}
.bx-chart-arc-svg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  display: block;
}
.bx-chart-arc-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.bx-chart-arc-track {
  fill: none;
  stroke: color-mix(in oklab, var(--d-ink) 6%, transparent);
  /* stroke-width is set inline on the path so it can scale with
     branch count alongside the value rings. */
  stroke-linecap: round;
}
.bx-chart-arc-tick {
  stroke: color-mix(in oklab, var(--d-ink) 26%, transparent);
  stroke-width: 1;
  stroke-dasharray: 1 2;
}
.bx-chart-arc-ticklbl {
  /* HTML overlay — fixed CSS font-size matching the trend panel's
     X-axis labels (clamp(8px, 0.72vw, 10px)) so the two charts
     read at the same scale across every viewport instead of the
     arc's labels scaling with the SVG. */
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: var(--fsz-chart-label); /* was clamp(8px,0.72vw,10px) — snapped to chart-label tier */
  color: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
}
.bx-chart-arc-val {
  font-size: var(--fsz-caption);
  font-weight: 700;
  fill: var(--d-ink);
  font-variant-numeric: tabular-nums;
}
/* HTML span (was SVG <text>) — uses color, not fill. */
.bx-chart-arc-center-num {
  /* Scale with panel width (container-query unit cqw on the panel
     container's content box). Floor 16px so the number stays
     legible at narrow 4-up panels, ceiling 24px so it reads
     clearly at typical desktop without overflowing the stage's
     bottom edge. Multiplier 11cqw so a typical 4-up panel
     (~238px content) hits the ceiling. */
  font-size: clamp(16px, 11cqw, 24px);
  font-weight: 800;
  color: var(--d-ink);
  line-height: 1;
}
.bx-chart-arc-center-lbl {
  /* Floor at chart-label tier (10px); cap at label (11px is a
     legacy half-step — leaving the cap for visual consistency
     with the arc num above). */
  font-size: clamp(var(--fsz-chart-label), 4.5cqw, 11px);
  /* Tight line-height matches the num so the total text height is
     predictable across viewports — default 1.4× was adding an
     extra ~3-5 CSS px that pushed the text past the stage bottom
     at narrow stage heights. */
  line-height: 1;
  color: var(--d-ink-3);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ── Panel: Contribution to Dubai (2-arc donut) ─────────────── */
/* Pie wrap stays in normal flow inside .bx-split-rows so the donut
   centers within the chart body area (not the panel). Flex centers
   the SVG horizontally and vertically inside the body. */
.bx-chart-pie-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
}
.bx-chart-pie-rest {
  fill: color-mix(in oklab, var(--d-ink) 7%, transparent);
  stroke: var(--d-bg-card);
  stroke-width: 1;
}
/* Active-branch top-right badge — small pill that surfaces the
   selected branch's name + institute context. Sits absolutely
   over the panel header so it doesn't push content around. */
.bx-split-panel { position: relative; }
.bx-chart-panel-badge {
  position: absolute;
  inset-block-start: 10px;
  inset-inline-end: 12px;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-inline-size: 60%;
  padding: 3px 8px 3px 6px;
  background: color-mix(in oklab, var(--d-ink) 4%, var(--d-bg-card));
  border: 1px solid var(--d-line);
  border-radius: 999px;
  font-size: var(--fsz-caption);
  color: var(--d-ink);
  pointer-events: none;
}
.bx-chart-panel-badge-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.bx-chart-panel-badge-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bx-chart-panel-badge-of {
  color: var(--d-ink-3);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Outcomes-row inline tip — anchored above the row's bar, replaces
   the global cursor-following ChartTip for the outcomes panel.
   Shows the same content whether the branch is hovered or pinned. */
.bx-chart-row-track { position: relative; }
.bx-chart-row-tip {
  position: absolute;
  inset-block-end: calc(100% + 8px);
  inset-inline-start: 50%;
  transform: translateX(-50%);
  z-index: 30;
  inline-size: max-content;
  min-inline-size: 180px;
  max-inline-size: 280px;
  padding: 8px 10px;
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: 8px;
  box-shadow: 0 8px 20px -8px rgba(15, 17, 21, 0.18);
  font-size: var(--fsz-label);
  color: var(--d-ink);
  line-height: 1.5;
  pointer-events: none;
}
.bx-chart-row-tip::after {
  content: '';
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  inline-size: 0;
  block-size: 0;
  border-inline: 6px solid transparent;
  border-block-start: 6px solid var(--d-bg-card);
}
.bx-chart-pie-svg {
  inline-size: 100%;
  block-size: 100%;
  display: block;
  max-inline-size: 250px;
  /* Cap height too — the donut is square so without a vertical
     cap it dominates at narrow viewports (the arc panel
     letterboxes vertically while the donut fills the full panel
     height). 200px keeps both panels visually balanced. */
  max-block-size: 200px;
}
/* Pie wrap is the relative anchor for the center HTML overlay. */
.bx-chart-pie-wrap { position: relative; }
.bx-chart-pie-center {
  position: absolute;
  /* Physical `left/top` 50% with `translate(-50%, -50%)` so the
     centre stays under the donut hole in both LTR and RTL. SVG
     geometry is direction-agnostic. */
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  pointer-events: none;
}
/* HTML span (was SVG <text>) — uses color, not fill. 22px to
   match the unified hero scale across digest cards. */
.bx-chart-pie-center-num {
  font-size: var(--fsz-h1);
  font-weight: 800;
  color: var(--d-ink);
  line-height: 1;
}
/* HTML span (was SVG <text>) — uses color, not fill. */
.bx-chart-pie-center-lbl {
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Smaller drill-down line shown under the main number when a
   branch is active (e.g. "2.45% of Dubai"). */
.bx-chart-pie-center-sub {
  font-size: var(--fsz-chart-label);
  color: var(--d-ink-2);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* ── Panel 3: Test outcomes rows ────────────────────────────── */
/* Header row labels the value column (e.g. "Pass Rate") so the
   trailing percentage reads as something specific, not an
   anonymous figure. */
.bx-chart-rows-head {
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) minmax(56px, auto);
  align-items: end;
  gap: 10px;
  padding: 0 8px 6px 8px;
  border-block-end: 1px solid var(--d-line-soft);
  margin-block-end: 4px;
}
.bx-chart-rows-headlbl {
  font-size: var(--fsz-caption);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--d-ink-3);
  text-align: end;
}
.bx-chart-rows {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
  min-block-size: 0;
  overflow: auto;
}
.bx-chart-row {
  --row-color: var(--d-indigo);
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr) minmax(50px, auto);
  align-items: center;
  gap: 8px;
  padding: 3px 6px;
  border-radius: 6px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background-color 140ms ease, border-color 140ms ease, opacity 140ms ease;
}
.bx-chart-row:hover,
.bx-chart-row.is-hov {
  background: color-mix(in oklab, var(--row-color) 10%, transparent);
  border-color: color-mix(in oklab, var(--row-color) 35%, var(--d-line));
}
.bx-chart-row.is-dim {
  opacity: 0.35;
}
.bx-chart-row-dot {
  inline-size: 12px;
  block-size: 12px;
  border-radius: 50%;
  background: var(--row-color);
}
.bx-chart-row-track {
  position: relative;
  block-size: 12px;
  background: color-mix(in oklab, var(--d-ink) 4%, transparent);
  border-radius: 999px;
  padding: 2px;
}
.bx-chart-row-bar {
  position: absolute;
  inset: 2px;
  display: flex;
  gap: 3px;
  border-radius: 999px;
  overflow: hidden;
}
/* Target tick — vertical line at the institute weighted Pass Rate.
   Sits on top of the bar at the target % position. */
.bx-chart-row-target {
  position: absolute;
  inset-block: -3px;
  inline-size: 2px;
  background: var(--d-ink);
  transform: translateX(-50%);
  z-index: 2;
  border-radius: 1px;
  pointer-events: none;
}
.bx-chart-seg {
  display: block;
  block-size: 100%;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--d-bg-card) 30%, transparent);
  transition: background-color 160ms ease, box-shadow 160ms ease;
}
/* Match L1 list-view rate-bar exactly: pass stays full strength;
   fail / absent / pending use *-border tokens by default and snap
   to full tokens on row hover / pin. */
.bx-chart-seg.is-pass    { background: var(--pass); }
.bx-chart-seg.is-fail    { background: var(--fail-border); }
.bx-chart-seg.is-absent  { background: var(--absent-border); }
.bx-chart-seg.is-pending { background: var(--pending-border); }
.bx-chart-row:hover .bx-chart-seg.is-fail,
.bx-chart-row.is-hov .bx-chart-seg.is-fail    { background: var(--fail); }
.bx-chart-row:hover .bx-chart-seg.is-absent,
.bx-chart-row.is-hov .bx-chart-seg.is-absent  { background: var(--absent); }
.bx-chart-row:hover .bx-chart-seg.is-pending,
.bx-chart-row.is-hov .bx-chart-seg.is-pending { background: var(--pending); }
.bx-chart-row-val {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--d-ink);
  text-align: end;
  font-variant-numeric: tabular-nums;
}
.bx-chart-row-val.is-over  { color: var(--d-ok); }
.bx-chart-row-val.is-under { color: var(--d-err); }
.bx-chart-row-legend {
  list-style: none;
  margin: 10px 0 0 0;
  padding: 8px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  border-block-start: 1px solid var(--d-line-soft);
}
.bx-chart-row-legend li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-2);
}
.bx-chart-seg-sw {
  inline-size: 14px;
  block-size: 8px;
  border-radius: 999px;
  flex-shrink: 0;
}
.bx-chart-seg-sw.is-pass    { background: var(--pass); }
.bx-chart-seg-sw.is-fail    { background: var(--fail); }
.bx-chart-seg-sw.is-absent  { background: var(--absent); }
.bx-chart-seg-sw.is-pending { background: var(--pending); }
.bx-chart-row-legend-tick {
  display: inline-block;
  inline-size: 2px;
  block-size: 12px;
  background: var(--d-ink);
  border-radius: 1px;
  flex-shrink: 0;
}

/* ── Panel: Capacity radar (single chart, all branches) ────── */
.bx-chart-radar-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
}
.bx-chart-radar-svg {
  inline-size: 100%;
  block-size: 100%;
  display: block;
  max-inline-size: 280px;
}
.bx-chart-radar-ring {
  fill: none;
  stroke: var(--d-line-soft);
  stroke-width: 1;
}
.bx-chart-radar-spoke {
  stroke: var(--d-line-soft);
  stroke-width: 1;
}
.bx-chart-radar-axis {
  font-size: var(--fsz-caption);
  fill: var(--d-ink-2);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* ════════════════════════════════════════════════════════════════
   BRANCH COMPARISON TABLE
   ════════════════════════════════════════════════════════════════ */
.branch-cmp {
  /* L2 Branches section + L3 Branch Performance section share this
     container class. Light mode keeps --bg-raised (near-white).
     Dark mode drops to --d-bg-page so the inner table / chart
     panels (which use --d-bg-card / --bg-raised) elevate visibly
     above. Same dark-only elevation pattern as .d2-section and
     .branch-deep above. */
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 16px; background: var(--bg-raised);
  border: 1px solid var(--line); border-radius: var(--r-lg);
}
[data-theme="dark"] .branch-cmp {
  background: var(--d-bg-page);
}
.branch-cmp-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap;
}
.branch-cmp-title-row {
  display: inline-flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
}
/* Same small-caps gray label treatment as .d2-section-title — the
   T2 Branches section is a main-column section, so it follows the
   same hierarchy. (Left-column digest cards keep their original
   bolder dark titles per the design call — those carry an icon
   and live at a finer scale.) */
.branch-cmp-title {
  margin: 0;
  font-size: var(--fsz-label);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-3);
}
[dir="rtl"] .branch-cmp-title { font-size: var(--fsz-label); }
.branch-cmp-hint {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fsz-caption); color: var(--ink-3);
}
.branch-cmp-hint > svg { color: var(--ink-3); flex-shrink: 0; }
.branch-cmp-tbl-wrap {
  /* No overflow:hidden — corners are rounded via the per-cell
     border-radius pattern (see thead/tbody first-/last-child rules
     below), which lets floating elements like the rate-cell hover
     popover escape vertically without being clipped.
     overflow-x: auto so the table scrolls horizontally only once
     the sum of column min-widths exceeds the wrap's content width.
     Each column carries its own min so bar-diagram columns absorb
     shrinkage first; the name column is min-pinned so it never
     truncates. */
  border: 1px solid var(--tbl-frame-border);
  border-radius: var(--tbl-frame-radius);
  background: var(--bg-raised);
  overflow-x: auto;
}
.branch-cmp-tbl { inline-size: 100%; border-collapse: collapse; font-size: var(--fsz-body); }
[dir="rtl"] .branch-cmp-tbl { font-size: var(--fsz-body); }
/* Mirror .data-table thead th sizing so the branches table head reads
   the same height/density as savedtests / reports. The .th-btn child
   (rendered by BranchTh) owns its own padding (10px 6px) so the th
   itself is padding-zero / fixed-height. */
.branch-cmp-tbl thead th {
  text-align: start;
  background: var(--tbl-header-bg);
  /* Stronger section border between header and body. */
  border-block-end: 1px solid var(--tbl-section-border);
  cursor: pointer; user-select: none;
  padding: 0;
  height: 40px;
  font-weight: 500;
  color: var(--tbl-header-fg);
  position: sticky; inset-block-start: 0; z-index: 2;
}
.branch-cmp-tbl thead th.th-arrow { inline-size: 32px; cursor: default; }
.branch-cmp-tbl thead th .sort-i { color: var(--tbl-sort-active); margin-inline-start: 4px; }
.branch-cmp-tbl tbody td {
  /* Body cells are left-aligned across all columns (per design call —
     numerics use tabular-nums so the leading digit is the column
     anchor). */
  padding: 10px 12px; border-block-end: 1px solid var(--tbl-divider);
  color: var(--tbl-cell-fg); vertical-align: middle;
  text-align: start;
}
.branch-cmp-tbl tbody td.tnum { font-variant-numeric: tabular-nums; color: var(--tbl-cell-strong); }
.branch-cmp-tbl tbody td.cell-name { font-weight: 600; color: var(--tbl-cell-strong); }
/* Name column gets a generous min so the leading text (institute /
   branch / examiner name) never truncates as the viewport narrows. */
.branch-cmp-tbl th.th-name,
.branch-cmp-tbl td.cell-name { min-inline-size: 220px; }
/* Pass-rate column — multi-segment bar. Min lets the bar shrink to
   ~160px before the wrap engages horizontal scroll; below that the
   bar segments would lose readability. */
.branch-cmp-tbl th.th-pr,
.branch-cmp-tbl td.cell-pr {
  /* Number stacks above the bar so the cell can be narrower than
     the previous side-by-side layout. */
  min-inline-size: 100px;
}
/* 1st Time Pass Rate cell — donut + small % label only, so the
   column can be far narrower than the segmented Pass Rate column. */
.branch-cmp-tbl th.th-firsttime-pr,
.branch-cmp-tbl td.cell-firsttime-pr { inline-size: 110px; min-inline-size: 110px; }
/* Contribution column — single-bar ProgressBar. Min just keeps the
   bar wide enough to read as a bar (not a stub) before scroll
   engages. */
.branch-cmp-tbl th.th-contribution,
.branch-cmp-tbl td.cell-contribution { min-inline-size: 100px; }
.branch-cmp-tbl tbody td.cell-arrow {
  color: var(--ink-3);
  text-align: end;
  font-size: var(--fsz-h3);
  /* Chevron direction is JSX-driven via `lang === 'ar' ? '‹' : '›'`
     so the literal text content already carries the correct glyph
     per language — no ::before swap or font-size: 0 hack needed. */
}
.branch-cmp-tbl tbody tr:nth-child(even) { background: var(--tbl-row-stripe-bg); }
.branch-cmp-tbl tbody tr.is-drillable { cursor: pointer; transition: background var(--t-fast); }
.branch-cmp-tbl tbody tr.is-drillable:hover,
.branch-cmp-tbl tbody tr:nth-child(even).is-drillable:hover { background: var(--tbl-row-hover-bg); }
.branch-cmp-tbl tbody tr.is-drillable:hover td.cell-arrow { color: var(--brand-700); }
.branch-cmp-tbl tbody tr:last-child td { border-block-end: none; }
/* Round the table's outer corners by rounding the first/last cells in
   the corner rows. Avoids the wrapper-overflow tradeoff when tooltips
   need to escape vertically. */
.branch-cmp-tbl thead tr:first-child th:first-child { border-start-start-radius: var(--tbl-frame-radius); }
.branch-cmp-tbl thead tr:first-child th:last-child  { border-start-end-radius: var(--tbl-frame-radius); }
/* Square the top corners while the header is stuck/floating (toggled by
   the global observer in native-title-shim.js), same as .data-table —
   stops the page background bleeding through the rounded corner arcs.
   This wrapper is its own scroll container today so the header rarely
   floats, but the rule future-proofs it (e.g. if a max-height is added). */
.branch-cmp-tbl thead.is-stuck tr:first-child th:first-child { border-start-start-radius: 0; }
.branch-cmp-tbl thead.is-stuck tr:first-child th:last-child  { border-start-end-radius: 0; }
.branch-cmp-tbl tbody tr:last-child td:first-child  { border-end-start-radius: var(--tbl-frame-radius); }
.branch-cmp-tbl tbody tr:last-child td:last-child   { border-end-end-radius: var(--tbl-frame-radius); }
.branch-cmp-tbl th.th-trend { inline-size: 140px; }
.branch-cmp-tbl td.cell-trend { inline-size: 140px; padding: 10px 12px; }
.cell-trend-spark { display: block; inline-size: 100%; block-size: 24px; }
.branch-cmp-tbl td.cell-histo { inline-size: 200px; padding: 8px 12px; }

/* ════════════════════════════════════════════════════════════════
   ALERT STRIP
   ════════════════════════════════════════════════════════════════ */
.alert-strip {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; border: 1px solid var(--line);
  border-radius: var(--r-lg); background: var(--bg-raised);
  transition: background var(--t-med), border-color var(--t-med);
}
.alert-strip.is-clear { color: var(--ink-3); }
.alert-strip.is-clear svg { color: var(--ok); }
.alert-strip-text { display: flex; flex-direction: column; gap: 2px; }
.alert-strip-text strong { color: var(--ink); font-weight: 600; font-size: var(--fsz-body); }
.alert-strip-text span { font-size: var(--fsz-label); color: var(--ink-3); }

.alert-strip.is-active {
  background: color-mix(in oklab, var(--err) 10%, var(--bg-raised));
  border-color: var(--err); border-inline-start-width: 4px;
  align-items: flex-start;
  animation: alert-strip-in 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes alert-strip-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.alert-strip-icon {
  display: grid; place-items: center;
  inline-size: 32px; block-size: 32px;
  border-radius: var(--r-md); background: var(--err);
  color: #fff; flex-shrink: 0; margin-block-start: 2px;
}
.alert-strip-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.alert-strip-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.alert-strip-tag {
  font-size: var(--fsz-caption); text-transform: uppercase; letter-spacing: 0.06em;
  font-weight: 700; color: var(--err);
  padding: 2px 8px; background: color-mix(in oklab, var(--err) 18%, var(--bg-raised));
  border-radius: var(--r-pill);
}
.alert-strip-meta { font-size: var(--fsz-label); color: var(--ink-3); }
.alert-strip-spacer { flex: 1; }
.alert-strip-count {
  font-size: var(--fsz-caption); font-weight: 600; color: var(--ink);
  padding: 2px 8px; background: var(--bg-raised);
  border: 1px solid var(--line); border-radius: var(--r-pill);
}
.alert-strip-msg { font-size: var(--fsz-body); color: var(--ink); line-height: 1.5; }
[dir="rtl"] .alert-strip-msg { font-size: var(--fsz-body); }
.alert-strip-actions {
  display: flex; gap: 6px; flex-shrink: 0;
  align-items: flex-start; margin-block-start: 2px;
}
.alert-strip-btn {
  /* sm CTA tier — 4×10 padding + r-sm radius, aligns with
     `.alert-toast-btn` (sibling action surface) and the canonical
     `.btn-sm`. Previously 5×11 + r-md, which read inconsistent
     with the toast variant in the same alert family. */
  padding: 4px 10px; font: inherit; font-size: var(--fsz-label); font-weight: 500;
  border: 1px solid var(--line); background: var(--bg-raised);
  color: var(--ink-2); border-radius: var(--r-sm);
  cursor: pointer; white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.alert-strip-btn:hover { background: var(--bg-muted); color: var(--ink); }
.alert-strip-btn.is-primary { background: var(--err); color: #fff; border-color: var(--err); }
.alert-strip-btn.is-primary:hover { background: color-mix(in oklab, var(--err) 88%, #000); }
@media (max-width: 720px) {
  .alert-strip.is-active { flex-direction: column; align-items: stretch; }
  .alert-strip-actions { justify-content: flex-end; }
}

/* ── Branch activity feed (T3) ────────────────────────────────────
   Ports the mobility dashboard's `.ive-feed` pattern into page.css
   so the T3 BranchDashboard can render an activity timeline without
   pulling in the whole dashboard.css (which is mobility-only).      */
.branch-activity-feed {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  position: relative;
}
/* Per-LI line segment connecting this dot to the next — replaces
   the OL-wide ::before pseudo (which was clipped by the panel's
   overflow:auto when the feed scrolled). Each segment goes from
   this dot's center (LI top + 14 + 6 = 20px) to the next dot's
   center, i.e. 20px BELOW the LI's bottom. The last LI omits the
   segment so the line ends cleanly at the final dot. */
.branch-activity-feed::before { display: none; }
.branch-activity-feed .ive-feed-item {
  position: relative;
  padding-block: 8px;
  padding-inline-start: 22px;
  display: flex;
  gap: 8px;
}
.branch-activity-feed .ive-feed-item::before {
  content: '';
  position: absolute;
  /* Line center must align with dot center. Dot is at inset-inline-
     start: 0 with width 12px, so center is at 6px. Line is 2px wide
     so its inset-inline-start = 5px puts its center at 6px too. */
  inset-inline-start: 5px;
  inset-block-start: 20px;
  inset-block-end: -20px;
  inline-size: 2px;
  background: var(--line);
  z-index: 0;
}
.branch-activity-feed .ive-feed-item:last-child::before { display: none; }
.branch-activity-feed .ive-feed-dot {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 14px;
  inline-size: 12px;
  block-size: 12px;
  border-radius: 50%;
  background: var(--ink-3);
  border: 2px solid var(--bg-raised);
  box-shadow: 0 0 0 2px var(--line);
  z-index: 1;
}
.branch-activity-feed .ive-feed-item.is-ok   .ive-feed-dot { background: var(--ok);    box-shadow: 0 0 0 2px color-mix(in srgb, var(--ok)    35%, transparent); }
.branch-activity-feed .ive-feed-item.is-warn .ive-feed-dot { background: var(--warn);  box-shadow: 0 0 0 2px color-mix(in srgb, var(--warn)  35%, transparent); }
.branch-activity-feed .ive-feed-item.is-err  .ive-feed-dot { background: var(--err);   box-shadow: 0 0 0 2px color-mix(in srgb, var(--err)   35%, transparent); }
.branch-activity-feed .ive-feed-item.is-info .ive-feed-dot { background: var(--chart-1); box-shadow: 0 0 0 2px color-mix(in srgb, var(--chart-1) 35%, transparent); }
.branch-activity-feed .ive-feed-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.branch-activity-feed .ive-feed-line {
  display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: baseline;
  font-size: var(--fsz-body);
}
.branch-activity-feed .ive-feed-who { font-weight: 600; color: var(--ink); }
.branch-activity-feed .ive-feed-text { color: var(--ink-2); }
/* Feed refs carry `.ident`, which supplies the REAL monospace face
   (JetBrains Mono / --font-mono), 0.92em sizing, and tabular numerals so
   `#…`/code refs read as crisp identifiers. Here we only add the feed's
   accent color on top of that shared recipe. */
.branch-activity-feed .ive-feed-ref { color: var(--chart-2, var(--d-indigo)); font-variant-numeric: tabular-nums; }
.branch-activity-feed .ive-feed-meta { font-size: var(--fsz-label); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em; }
[dir="rtl"] .branch-activity-feed .ive-feed-meta { letter-spacing: 0; text-transform: none; }

/* Activity feed filter pills — compact tab row above the feed.
   Each pill maps to one tone (all / ok / warn / err / info); active
   pill carries the matching tone color so the filter selection is
   visible at a glance. */
.branch-activity-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-block-end: 8px;
}
.branch-activity-filter {
  appearance: none;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-2);
  border-radius: var(--r-pill);
  padding: 3px 9px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  line-height: 1.4;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.branch-activity-filter:hover { background: color-mix(in srgb, var(--ink) 5%, transparent); color: var(--ink); }
.branch-activity-filter:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.branch-activity-filter.is-active {
  background: var(--ink);
  color: var(--bg-card, var(--bg-raised));
  border-color: var(--ink);
}
.branch-activity-filter.is-active.is-tone-ok   { background: var(--ok);   border-color: var(--ok);   color: white; }
.branch-activity-filter.is-active.is-tone-warn { background: var(--warn); border-color: var(--warn); color: white; }
.branch-activity-filter.is-active.is-tone-err  { background: var(--err);  border-color: var(--err);  color: white; }
.branch-activity-filter.is-active.is-tone-info { background: var(--chart-1); border-color: var(--chart-1); color: white; }

/* In the panel-header `actions` slot the pills sit on the same row
   as the title — tighten padding so the row stays compact. */
.bx-split-panel-actions .branch-activity-filters { margin-block-end: 0; }
.bx-split-panel-actions .branch-activity-filter {
  padding: 1px 7px;
  font-size: var(--fsz-label);
  line-height: 1.5;
}

/* ── Three-up chart grid (T3 single-branch) ───────────────────────
   Three side-by-side panels on T3: Test Volume trend, KPI block,
   Activity timeline. Activity is intentionally a touch wider than
   the two chart panels (1.4fr) since it carries a list rather than
   a single visual. Below 1100px the layout drops to a 1-column
   stack so each panel still breathes.                                */
.bx-chart.bx-chart--three-up .bx-chart-grid--three-up {
  grid-template-columns: 1fr 1fr 1.4fr;
  gap: 14px;
}
.bx-chart.bx-chart--three-up .bx-chart-grid--three-up > .bx-split-panel {
  aspect-ratio: auto;
  /* Match the L2 BranchesChart panel height — at desktop the L2
     grid is 4×1 with 1:1 aspect, so each panel is ~280px tall on a
     typical viewport. Locking the L3 row to that height keeps the
     two views visually consistent. */
  block-size: 282px;
  min-block-size: 282px;
}
/* Visual order — DOM order is Test Volume, KPI block, Activity, but
   the visual layout places KPI block first, Test Volume in the
   middle, Activity last (which keeps the wider 1.4fr column at the
   end of the row). Using CSS `order` so the DOM stays in a sensible
   reading order while the user sees the requested arrangement. */
.bx-chart.bx-chart--three-up .bx-chart-grid--three-up > .bx-split-panel:nth-child(1) { order: 2; }
.bx-chart.bx-chart--three-up .bx-chart-grid--three-up > .bx-split-panel:nth-child(2) { order: 1; }
.bx-chart.bx-chart--three-up .bx-chart-grid--three-up > .bx-split-panel:nth-child(3) { order: 3; }
@media (max-width: 1100px) {
  .bx-chart.bx-chart--three-up .bx-chart-grid--three-up {
    grid-template-columns: 1fr 1fr;
    grid-auto-rows: 320px;
  }
  /* Activity spans the full second row when stacked into 2-col. */
  .bx-chart.bx-chart--three-up .bx-chart-grid--three-up > .bx-split-panel:nth-child(3) {
    grid-column: 1 / -1;
  }
}
@media (max-width: 720px) {
  .bx-chart.bx-chart--three-up .bx-chart-grid--three-up {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── Branch KPI block (T3 metrics panel) ──────────────────────────
   Three KPI rows stacked vertically inside the middle panel. Each
   metric uses a different visual treatment so they don't compete:
     1) Pass Rate → segmented horizontal bar
     2) 1st-Time Pass → area sparkline
     3) Contribution → mini ring                                       */
.bx-kpi-block {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 18px;
  block-size: 100%;
  justify-content: space-around;
  min-block-size: 0;
}
/* Each KPI row stacks header (label · value) directly above the
   visualization with a tight 4px gap. The bigger 18px row gap on
   `.bx-kpi-block` provides the breathing room BETWEEN rows so the
   value never visually attaches to the next row's viz. */
.bx-kpi-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  cursor: default;
}
.bx-kpi-row:focus-visible {
  /* Keyboard focus ring — uses the brand outline pattern from the
     design system. The row is interactive (drives the hover tip on
     focus), so a visible focus indicator is required for
     accessibility. */
  outline: 2px solid var(--brand-500, var(--d-indigo, #4f46e5));
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
/* Hover tooltip positioning override — the shared rate-cell-pop
   `is-floating` rule clears the logical inset properties on the
   tooltip so the inline `left` / `bottom` from the portal call
   wins. The .bx-kpi-pop class is just a hook for any future per-
   KPI styling without re-styling the shared popover chrome. */
/* KPI tooltip sizes to its content (label / number / percent fit
   comfortably at ~200px). The shared `.rate-cell-pop` rule already
   caps inline-size at `max-content`; we just lower the min so the
   tooltip doesn't expand to match the row width below it. */
.bx-kpi-pop {
  min-inline-size: 200px;
  inline-size: max-content;
  max-inline-size: 260px;
}
.bx-kpi-info {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.bx-kpi-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
}
.bx-kpi-value {
  font-size: var(--fsz-h3);
  font-weight: 700;
  color: var(--ink);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
/* Pass Rate variant — segmented horizontal bar, thin to match the
   visual weight of the spark + progress bars in the two rows below.
   8px tall is the sweet spot — reads as a chart, not chrome, and
   pairs cleanly with the spark's 32px and progress bar's 8px. */
.bx-kpi-segbar {
  display: flex;
  gap: 2px;
  block-size: 8px;
  inline-size: 100%;
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--bg-muted, color-mix(in oklab, var(--ink-3) 10%, transparent));
}
.bx-kpi-segbar-seg { display: block; block-size: 8px; transition: inline-size 280ms ease, background 180ms ease; }
.bx-kpi-segbar-seg.is-pass    { background: var(--pass, var(--d-ok, #15803d)); }
.bx-kpi-segbar-seg.is-fail    { background: var(--fail, var(--d-err, #b91c1c)); }
.bx-kpi-segbar-seg.is-absent  { background: var(--absent, var(--ink-3)); }
.bx-kpi-segbar-seg.is-pending { background: var(--pending, var(--d-warn, #b45309)); }
/* 1st-Time Pass variant — area sparkline matches the L1 KPI cards. */
.bx-kpi-row--spark .bx-kpi-spark-lg {
  inline-size: 100%;
  block-size: 32px;
  display: block;
}
/* Wrap captures pointer events for per-bucket hover (cross-hair +
   tooltip portaled to body). cursor:crosshair signals to the user
   that the chart is scrubbable along the x-axis. */
.bx-kpi-spark-wrap {
  position: relative;
  inline-size: 100%;
  cursor: crosshair;
}
.bx-kpi-spark-tip {
  /* Translate so the tip floats above the cursor without covering
     the data point. */
  transform: translate(-50%, -100%);
  pointer-events: none;
}
/* Contribution variant — single-fill progress bar showing the
   branch's share of the institute total. Same height as the
   segmented bar so the three rows line up visually. */
.bx-kpi-progress {
  position: relative;
  inline-size: 100%;
  block-size: 8px;
  border-radius: var(--r-pill);
  background: var(--bg-muted, color-mix(in oklab, var(--ink-3) 10%, transparent));
  overflow: hidden;
}
.bx-kpi-progress-fill {
  display: block;
  block-size: 8px;
  background: var(--d-indigo, #4f46e5);
  border-radius: var(--r-pill);
  transition: inline-size 280ms ease;
}

/* Activity feed inside the embedded SplitPanel — scrolls vertically
   so the list never blows out the panel height (matched to L2
   panels at ~282px, so the feed gets ~200px of usable space). The
   inline padding ensures the timeline dots' box-shadow rings clear
   the panel's overflow:hidden edge on the dot side, and the scroll
   gutter on the opposite side. */
.bx-split-panel .branch-activity-feed {
  max-block-size: 100%;
  overflow-y: auto;
  padding-inline-start: 4px;
  padding-inline-end: 6px;
}

/* ── AlertToast — floating alert stack ────────────────────────────
   Replaces the old inline `.alert-strip` placement on T2/T3. The
   stack is portaled into <body> and pinned to the top inline-end
   corner of the viewport. Each toast has a severity-driven dwell:
   info/ok auto-dismiss after 6s (progress bar at the bottom shows
   remaining time); warn/err stay until manually closed.            */
.alert-toast-stack {
  position: fixed;
  inset-block-start: 78px;            /* clears the topnav */
  inset-inline-end: 16px;
  z-index: var(--z-tooltip, 2000);
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
  max-inline-size: min(420px, calc(100vw - 32px));
}
.alert-toast {
  pointer-events: auto;
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: 0 18px 40px -16px rgba(15, 17, 21, 0.32), 0 2px 6px -2px rgba(15, 17, 21, 0.12);
  overflow: hidden;
  animation: alert-toast-in 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes alert-toast-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
.alert-toast.is-warn,
.alert-toast.is-err {
  border-inline-start-width: 4px;
  border-inline-start-color: var(--err);
  background: color-mix(in oklab, var(--err) 8%, var(--bg-raised));
}
.alert-toast.is-warn { border-inline-start-color: var(--warn, #b45309); }
.alert-toast.is-info { border-inline-start-width: 4px; border-inline-start-color: var(--d-indigo, #4f46e5); }
.alert-toast.is-ok   { border-inline-start-width: 4px; border-inline-start-color: var(--ok, #15803d); }
.alert-toast-icon {
  display: grid;
  place-items: center;
  inline-size: 28px;
  block-size: 28px;
  border-radius: var(--r-md);
  color: #fff;
  background: var(--err);
  flex-shrink: 0;
  margin-block-start: 2px;
}
.alert-toast.is-warn .alert-toast-icon { background: var(--warn, #b45309); }
.alert-toast.is-info .alert-toast-icon { background: var(--d-indigo, #4f46e5); }
.alert-toast.is-ok   .alert-toast-icon { background: var(--ok, #15803d); }
.alert-toast-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; padding-inline-end: 22px; }
.alert-toast-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.alert-toast-tag {
  font-size: var(--fsz-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 700;
  color: var(--err);
  padding: 2px 8px;
  background: color-mix(in oklab, var(--err) 18%, var(--bg-raised));
  border-radius: var(--r-pill);
}
.alert-toast.is-warn .alert-toast-tag { color: var(--warn, #b45309); background: color-mix(in oklab, var(--warn, #b45309) 18%, var(--bg-raised)); }
.alert-toast.is-info .alert-toast-tag { color: var(--d-indigo, #4f46e5); background: color-mix(in oklab, var(--d-indigo, #4f46e5) 18%, var(--bg-raised)); }
.alert-toast.is-ok   .alert-toast-tag { color: var(--ok, #15803d); background: color-mix(in oklab, var(--ok, #15803d) 18%, var(--bg-raised)); }
.alert-toast-meta { font-size: var(--fsz-label); color: var(--ink-3); }
.alert-toast-msg { font-size: var(--fsz-body); color: var(--ink); line-height: 1.4; }
.alert-toast-actions { display: flex; gap: 6px; margin-block-start: 2px; }
.alert-toast-btn {
  /* sm CTA tier — radius unified to r-sm (was r-md). Matches the
     sibling `.alert-strip-btn` and the canonical `.btn-sm`. */
  padding: 4px 10px;
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  border: 1px solid var(--line);
  background: var(--bg-raised);
  color: var(--ink-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.alert-toast-btn:hover { background: var(--bg-muted); color: var(--ink); }
.alert-toast-btn.is-primary { background: var(--err); color: #fff; border-color: var(--err); }
.alert-toast.is-warn .alert-toast-btn.is-primary { background: var(--warn, #b45309); border-color: var(--warn, #b45309); }
.alert-toast.is-info .alert-toast-btn.is-primary { background: var(--d-indigo, #4f46e5); border-color: var(--d-indigo, #4f46e5); }
.alert-toast.is-ok   .alert-toast-btn.is-primary { background: var(--ok, #15803d); border-color: var(--ok, #15803d); }
.alert-toast-btn.is-primary:hover { filter: brightness(0.92); }
.alert-toast-close {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
  inline-size: 22px;
  block-size: 22px;
  display: grid;
  place-items: center;
  border: 0;
  background: transparent;
  color: var(--ink-3);
  border-radius: var(--r-xs);
  cursor: pointer;
  padding: 0;
  transition: background var(--t-fast), color var(--t-fast);
}
.alert-toast-close:hover { background: var(--bg-muted); color: var(--ink); }
.alert-toast-progress {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  block-size: 3px;
  background: var(--err);
  transform-origin: left center;
  pointer-events: none;
}
[dir="rtl"] .alert-toast-progress { transform-origin: right center; }
.alert-toast.is-warn .alert-toast-progress { background: var(--warn, #b45309); }
.alert-toast.is-info .alert-toast-progress { background: var(--d-indigo, #4f46e5); }
.alert-toast.is-ok   .alert-toast-progress { background: var(--ok, #15803d); }

/* ════════════════════════════════════════════════════════════════
   BRANCH DEEP CARDS
   ════════════════════════════════════════════════════════════════ */
.branch-deep {
  /* Light mode keeps `--bg-raised` (near-white) — operator preference
     was to leave light mode untouched. The dark-mode container lift
     is applied via the [data-theme="dark"] override below so
     containers visibly elevate cards only when the dark token set
     would otherwise collapse container + content into one surface. */
  padding: 14px 16px; background: var(--bg-raised);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  display: flex; flex-direction: column; gap: 12px;
}
[data-theme="dark"] .branch-deep {
  /* In dark mode, --bg-raised would equal --d-bg-card (0.295), making
     the container indistinguishable from the institute cards inside.
     Drop to the page tier so cards visibly elevate above. */
  background: var(--d-bg-page);
}
.branch-deep-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding-block-end: 10px; border-block-end: 1px solid var(--line);
}
.branch-deep-title {
  margin: 0; font-size: var(--fsz-h3); font-weight: 600; color: var(--ink);
  display: inline-flex; align-items: center; gap: 8px;
  letter-spacing: -0.005em;
}
[dir="rtl"] .branch-deep-title { font-size: var(--fsz-h3); }
.branch-deep-meta {
  font-size: var(--fsz-label); color: var(--ink-3);
  display: inline-flex; align-items: center; gap: 6px;
}
.branch-deep-tools { display: inline-flex; align-items: center; gap: 8px; }

/* ─── Live tests grid ─────────────────────────────────────── */
.live-test-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 12px;
}
.live-test-card {
  display: flex; flex-direction: column; gap: 10px;
  padding: 12px 14px; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.live-test-head { display: flex; align-items: center; gap: 10px; }
.live-test-name { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.live-test-avatar {
  inline-size: 32px; block-size: 32px;
  border-radius: 50%; background: var(--brand-tint-2);
  color: var(--brand-700); display: grid; place-items: center;
  font-weight: 600; font-size: var(--fsz-body); flex-shrink: 0;
}
.live-test-name-block { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.live-test-student {
  font-size: var(--fsz-body); font-weight: 600; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .live-test-student { font-size: var(--fsz-body); }
.live-test-id { font-size: var(--fsz-caption); color: var(--ink-3); }
.live-test-attempt {
  display: flex; flex-direction: column; align-items: end;
  background: var(--bg-muted); padding: 4px 10px;
  border-radius: var(--r-md); flex-shrink: 0;
}
.live-test-attempt-label { font-size: var(--fsz-caption); text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-3); font-weight: 600; }
.live-test-attempt-num { font-size: var(--fsz-h3); font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; line-height: 1.1; }
.live-test-meta { font-size: var(--fsz-label); color: var(--ink-3); }
.live-test-meta-row strong { color: var(--ink); font-weight: 500; margin-inline-start: 4px; }
.live-test-progress {
  display: grid; grid-template-columns: auto auto 1fr; gap: 8px 12px;
  align-items: center; padding: 8px 10px;
  background: var(--bg-muted); border-radius: var(--r-md);
}
.live-test-vehicle {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fsz-label); color: var(--ink); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.live-test-elapsed { font-weight: 700; margin-inline-start: 4px; color: var(--brand-700); }
.live-test-status {
  font-size: var(--fsz-caption); font-weight: 700;
  padding: 2px 8px; border-radius: var(--r-pill);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.live-test-status.is-ongoing { background: var(--brand-700); color: var(--brand-ink); }
.live-test-maneuver {
  font-size: var(--fsz-label); color: var(--ink-2);
  grid-column: 1 / -1; font-weight: 500;
}
.live-test-foot { display: flex; justify-content: flex-end; }
.live-test-cta {
  font-size: var(--fsz-label);
  color: var(--brand-700);
  font-weight: 600;
  text-decoration: none;
  /* Unified inline-link treatment — color shift + soft brand bg
     tint on hover, never underline. Negative margin keeps the
     layout footprint identical so embedded use in cards / strips
     doesn't shift on hover. */
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.live-test-cta:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.live-test-cta:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* ─── Yard stations row ─────────────────────────────────── */
.yard-stations {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}
.yard-station {
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-md);
  position: relative;
}
.yard-station.is-ongoing {
  border-color: color-mix(in oklab, var(--brand-700) 35%, var(--bg));
  background: var(--brand-tint);
}
.yard-station.is-offline,
.yard-station.is-not-ready {
  background: color-mix(in oklab, var(--err) 8%, var(--bg));
  border-color: color-mix(in oklab, var(--err) 25%, var(--bg));
}
.yard-station-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.yard-station-id { font-size: var(--fsz-label); font-weight: 600; color: var(--ink); }
.yard-station-type {
  inline-size: 22px; block-size: 22px;
  display: grid; place-items: center; border-radius: var(--r-sm);
  background: var(--bg-muted); font-size: var(--fsz-caption); font-weight: 700; color: var(--ink-2);
}
.yard-station.is-ongoing .yard-station-type { background: var(--brand-700); color: var(--brand-ink); }
.yard-station-state {
  font-size: var(--fsz-caption); text-transform: uppercase; letter-spacing: 0.06em;
  font-weight: 700; color: var(--ink-3);
}
.yard-station.is-ready    .yard-station-state { color: var(--ok); }
.yard-station.is-ongoing  .yard-station-state { color: var(--brand-700); }
.yard-station.is-offline  .yard-station-state,
.yard-station.is-not-ready .yard-station-state { color: var(--err); }
.yard-station-subs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 3px 8px;
  margin-block-start: 4px;
}
.yard-sub {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fsz-caption); color: var(--ink-3);
}
.yard-sub-dot {
  display: inline-block; inline-size: 5px; block-size: 5px;
  border-radius: 50%; background: var(--ok); flex-shrink: 0;
}
.yard-sub.is-ok   .yard-sub-dot { background: var(--ok); }
.yard-sub.is-warn .yard-sub-dot { background: var(--warn); }
.yard-sub.is-down .yard-sub-dot { background: var(--err); }
.yard-sub.is-warn .yard-sub-label,
.yard-sub.is-down .yard-sub-label { color: var(--ink); font-weight: 500; }

/* ════════════════════════════════════════════════════════════════
   MANEUVERS, TOP10
   ════════════════════════════════════════════════════════════════ */
.man-bars { display: flex; flex-direction: column; gap: 10px; }
.man-bar-row {
  display: grid; grid-template-columns: 160px 1fr auto;
  align-items: center; gap: 14px; font-size: var(--fsz-label);
}
[dir="rtl"] .man-bar-row { font-size: var(--fsz-body); }
@media (max-width: 720px) { .man-bar-row { grid-template-columns: 110px 1fr auto; gap: 8px; } }
.man-bar-name {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--ink); font-weight: 500;
}
.man-bar-name svg { color: var(--brand-700); }
.man-bar-track {
  position: relative; block-size: 14px;
  background: var(--bg-muted); border-radius: var(--r-pill);
  display: flex; overflow: hidden;
}
.man-bar-pass { display: block; background: var(--ok); }
.man-bar-fail { display: block; background: var(--err); }
.man-bar-tick {
  position: absolute; inset-block-start: -3px; inset-block-end: -3px;
  inline-size: 2px; background: var(--ink);
  border: 1px solid var(--bg-raised); border-radius: 1px;
  transform: translateX(-1px);
}
.man-bar-vals {
  display: flex; align-items: center; gap: 10px;
  min-inline-size: 96px; justify-content: flex-end;
}
.man-bar-pr { font-weight: 700; font-variant-numeric: tabular-nums; font-size: var(--fsz-body); }
.man-bar-att {
  font-variant-numeric: tabular-nums; font-size: var(--fsz-caption);
  color: var(--ink-3); background: var(--bg-muted);
  padding: 2px 7px; border-radius: var(--r-pill); font-weight: 600;
}
.man-legend {
  display: flex; gap: 14px;
  margin-block-start: 8px; padding-block-start: 8px;
  border-block-start: 1px solid var(--line);
  font-size: var(--fsz-caption); color: var(--ink-3);
}
.man-legend-item { display: inline-flex; align-items: center; gap: 5px; }
.man-legend-sw { inline-size: 10px; block-size: 10px; border-radius: 2px; background: var(--ok); }
.man-legend-sw.is-fail { background: var(--err); }
.man-legend-sw.is-tick { background: var(--ink); }

/* .man-mode-toggle / .mmt-btn — retired (was unused). Use .seg-toggle.is-sm. */

.top10-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 1100px) { .top10-grid { grid-template-columns: 1fr; } }
.top10-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.top10-row {
  display: grid; grid-template-columns: 24px 1.5fr 1fr 56px 48px;
  align-items: center; gap: 10px; padding: 6px 8px;
  border-radius: var(--r-sm); font-size: var(--fsz-label);
  transition: background var(--t-fast);
}
[dir="rtl"] .top10-row { font-size: var(--fsz-body); }
.top10-row:hover { background: var(--bg-muted); }
.top10-rank {
  font-weight: 700; color: var(--ink-3); font-size: var(--fsz-label);
  text-align: center; font-variant-numeric: tabular-nums;
}
.top10-row:nth-child(1) .top10-rank,
.top10-row:nth-child(2) .top10-rank,
.top10-row:nth-child(3) .top10-rank { color: var(--err); }
.top10-name {
  color: var(--ink); font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.top10-bar {
  display: block; block-size: 8px;
  background: var(--bg-muted); border-radius: var(--r-pill); overflow: hidden;
}
.top10-fill {
  display: block; block-size: 100%; background: var(--err);
  border-radius: inherit; transition: inline-size var(--t-med);
}
.top10-fill.is-major { background: var(--err); }
.top10-fill.is-minor { background: var(--warn); }
.top10-share { text-align: end; font-variant-numeric: tabular-nums; color: var(--ink-3); font-size: var(--fsz-caption); }
.top10-count { text-align: end; font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 700; }

/* ════════════════════════════════════════════════════════════════
   CHART PRIMITIVES — shared styles
   ════════════════════════════════════════════════════════════════ */
/* Inverted contrast — tooltips read as a SEPARATE surface against the
   page chrome (GitHub / Linear / Stripe pattern). Light page → dark
   tooltip; dark page → light tooltip. Per-theme tokens at the bottom
   of the file: --tooltip-bg / --tooltip-fg / --tooltip-fg-muted /
   --tooltip-border / --tooltip-shadow. */
.chart-tip {
  /* Portaled tooltips sit at the top tier (above modals/toasts/popovers) —
     was 220, which let popovers like the wall layout menu (z 1000) cover
     the hover tooltips. --z-tooltip is exactly this tier. */
  position: fixed; z-index: var(--z-tooltip, 2000); pointer-events: none;
  background: var(--tooltip-bg);
  border: 1px solid var(--tooltip-border);
  box-shadow: var(--tooltip-shadow);
  padding: 8px 10px;
  border-radius: var(--r-md);
  /* Unified tooltip typography — every tooltip in the system reads at
     the same size + weight + family, regardless of which path it came
     through (ChartTip / HoverTip / [title] shim / [data-tip] shim /
     migrated popovers). Label tier (12px) with medium weight (500)
     for legibility against the inverted dark surface.
     The family chain appends `var(--font-ar)` so any Arabic glyph
     embedded in an LTR-context tooltip (e.g. the "switch to العربية"
     hint in the language toggle) renders with IBM Plex Sans Arabic
     via per-glyph font fallback, instead of dropping to the browser's
     default Arabic font (which mismatches the rest of the UI). The
     RTL cascade still rewrites `--font-ui` itself when the page
     direction is RTL, so the bilingual handling stays consistent. */
  font-family: var(--font-ui), var(--font-ar);
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--tooltip-fg);
  /* Shrink-to-fit width: no minimum, content determines size up to
     280px cap. Previously `min-inline-size: 160px` produced empty
     trailing space on short labels like "Notifications" or "العربية". */
  max-inline-size: 280px;
  inline-size: max-content;
  line-height: 1.5;
}
/* Pointer arrow — pseudo-element triangle pointing back to the
   trigger element. Two layered triangles: an outer one matching the
   border color and an inner one matching the bg, offset by 1px so the
   border continues onto the arrow without visible seams. The arrow's
   horizontal position is controlled by `--chart-tip-arrow-x` which
   ChartTip computes per anchor to align with the trigger's center.
   Only renders when `data-placement` is set (anchored mode); legacy
   cursor-follow callsites get no arrow. */
.chart-tip[data-placement]::before,
.chart-tip[data-placement]::after {
  content: '';
  position: absolute;
  inline-size: 0;
  block-size: 0;
  border: 7px solid transparent;
  left: calc(var(--chart-tip-arrow-x, 50%) - 7px);
}
.chart-tip[data-placement="top"]::before {
  bottom: -14px;
  border-block-start-color: var(--tooltip-border);
}
.chart-tip[data-placement="top"]::after {
  bottom: -13px;
  border-block-start-color: var(--tooltip-bg);
}
.chart-tip[data-placement="bottom"]::before {
  top: -14px;
  border-block-end-color: var(--tooltip-border);
}
.chart-tip[data-placement="bottom"]::after {
  top: -13px;
  border-block-end-color: var(--tooltip-bg);
}
/* Inline (side) placement — tooltip sits to the LEADING or TRAILING
   side of the trigger. Arrow renders on the side of the tooltip that
   faces the trigger, vertically centered via --chart-tip-arrow-y. Used
   for vertical lists where above/below would occlude adjacent items
   (sidenav icons in collapsed mode). The leading/trailing direction is
   logical — auto-flips with `dir="rtl"` via inset-inline-start/end. */
.chart-tip[data-placement="inline-end"]::before,
.chart-tip[data-placement="inline-end"]::after,
.chart-tip[data-placement="inline-start"]::before,
.chart-tip[data-placement="inline-start"]::after {
  left: auto;
  top: calc(var(--chart-tip-arrow-y, 50%) - 7px);
}
/* inline-end: tooltip is on the TRAILING side of the trigger, so the
   arrow lives on the tooltip's LEADING (start) edge pointing back. */
.chart-tip[data-placement="inline-end"]::before {
  inset-inline-start: -14px;
  border-inline-end-color: var(--tooltip-border);
}
.chart-tip[data-placement="inline-end"]::after {
  inset-inline-start: -13px;
  border-inline-end-color: var(--tooltip-bg);
}
/* inline-start: mirror — tooltip on the LEADING side, arrow on the
   tooltip's TRAILING (end) edge. */
.chart-tip[data-placement="inline-start"]::before {
  inset-inline-end: -14px;
  border-inline-start-color: var(--tooltip-border);
}
.chart-tip[data-placement="inline-start"]::after {
  inset-inline-end: -13px;
  border-inline-start-color: var(--tooltip-bg);
}
[dir="rtl"] .chart-tip { font-size: var(--fsz-label); }
/* Inner content reads against the inverted bg — use tooltip-scoped
   tokens (tooltip-fg, tooltip-fg-muted, tooltip-border) so the head /
   row / values stay legible on both dark-light and light-dark tooltips
   without per-theme overrides. */
.chart-tip-head {
  font-size: var(--fsz-caption); font-weight: 600;
  color: var(--tooltip-fg-muted);
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-block-end: 4px; padding-block-end: 4px;
  border-block-end: 1px solid var(--tooltip-border);
}
.chart-tip-row {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fsz-label);
  color: var(--tooltip-fg-muted);
  padding: 1px 0;
}
.chart-tip-row b {
  margin-inline-start: auto;
  color: var(--tooltip-fg);
  font-weight: 600;
}
.chart-tip-sw {
  display: inline-block; inline-size: 8px; block-size: 8px;
  border-radius: 2px; background: var(--brand-600); flex-shrink: 0;
}
/* Semantic swatches keep their tones (ok/err/warn) — green/red/amber
   read with high contrast against either inverted bg. The mid-tone
   swatches (pending/absent/muted) are nudged brighter on the dark
   tooltip surface so they don't muddy against the dark bg. */
.chart-tip-sw.is-pass    { background: var(--ok); }
.chart-tip-sw.is-fail    { background: var(--err); }
.chart-tip-sw.is-pending { background: var(--info); }
.chart-tip-sw.is-absent  { background: var(--tooltip-fg-muted); }
.chart-tip-sw.is-warn    { background: var(--warn); }
.chart-tip-sw.is-muted   { background: var(--tooltip-fg-muted); }
.chart-tip-row.is-cmp .chart-tip-sw {
  background: transparent;
  border: 1px dashed var(--tooltip-fg-muted);
}

/* Line chart */
.line-chart-wrap { display: flex; flex-direction: column; gap: 8px; }
.line-chart-legend {
  display: flex; gap: 14px;
  font-size: var(--fsz-caption); color: var(--ink-3);
  align-self: flex-end;
}
.line-chart-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.line-chart-legend-sw {
  inline-size: 14px; block-size: 2px;
  border-radius: 1px; background: var(--brand-600);
}
.line-chart-legend-item.is-cmp .line-chart-legend-sw {
  background: transparent;
  border-block-start: 1.5px dashed var(--ink-3); block-size: 0;
}
.line-chart { inline-size: 100%; block-size: 240px; display: block; }
.line-chart-grid line { stroke: var(--line); stroke-dasharray: 2 3; stroke-width: 1; opacity: 0.7; }
.line-chart-axis {
  font-size: var(--fsz-caption); fill: var(--ink-3);
  font-family: var(--font-ui); font-weight: 500;
}
.line-chart-area  { fill: var(--brand-600); fill-opacity: 0.10; }
.line-chart-line  { fill: none; stroke: var(--brand-600); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.line-chart-cmp-line {
  fill: none; stroke: var(--ink-3); stroke-width: 1.4;
  stroke-dasharray: 5 4; stroke-linecap: round;
}
.line-chart-cross { stroke: var(--brand-600); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; }
.line-chart-dot { fill: var(--bg-raised); stroke: var(--brand-600); stroke-width: 2; }
.line-chart-dot.is-cmp { stroke: var(--ink-3); }

/* Donut */
.donut-wrap { position: relative; display: inline-block; flex-shrink: 0; }
.donut { display: block; }
.donut-seg {
  stroke: var(--brand-600); stroke-linecap: butt;
  transition: stroke-width var(--t-fast); cursor: pointer;
}
.donut-seg.is-pass    { stroke: var(--ok); }
.donut-seg.is-fail    { stroke: var(--err); }
.donut-seg.is-pending { stroke: color-mix(in oklab, var(--brand-500) 60%, var(--ink-3)); }
.donut-seg.is-absent  { stroke: var(--ink-4); }
.donut-seg.is-warn    { stroke: var(--warn); }
.donut-center {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  pointer-events: none; text-align: center;
}
.donut-center-value {
  font-size: var(--fsz-h1); font-weight: 700; letter-spacing: -0.02em;
  color: var(--ink); line-height: 1; font-variant-numeric: tabular-nums;
}
.donut-center-label {
  font-size: var(--fsz-caption); color: var(--ink-3); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; margin-block-start: 3px;
}
.donut-legend {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
  flex: 1; min-inline-size: 0;
}
.donut-legend-row {
  display: grid; grid-template-columns: 10px 1fr auto auto;
  align-items: center; gap: 8px;
  font-size: var(--fsz-label); padding: 3px 0;
}
[dir="rtl"] .donut-legend-row { font-size: var(--fsz-label); }
.donut-legend-row + .donut-legend-row { border-block-start: 1px solid var(--line); }
.donut-legend-sw {
  inline-size: 10px; block-size: 10px;
  border-radius: 2px; background: var(--brand-600);
}
.donut-legend-sw.is-pass    { background: var(--ok); }
.donut-legend-sw.is-fail    { background: var(--err); }
.donut-legend-sw.is-pending { background: color-mix(in oklab, var(--brand-500) 60%, var(--ink-3)); }
.donut-legend-sw.is-absent  { background: var(--ink-4); }
.donut-legend-label { color: var(--ink-2); }
.donut-legend-value { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.donut-legend-share { color: var(--ink-3); font-variant-numeric: tabular-nums; min-inline-size: 36px; text-align: end; }

/* Heatmap */
.heatmap-wrap { overflow-x: auto; }
.heatmap { border-collapse: separate; border-spacing: 3px; inline-size: 100%; }
.heatmap thead th { padding-block-end: 4px; }
.heatmap-col {
  font-size: var(--fsz-caption); font-weight: 600; color: var(--ink-3);
  text-align: center; font-variant-numeric: tabular-nums;
}
.heatmap-row {
  font-size: var(--fsz-caption); font-weight: 600; color: var(--ink-3);
  text-align: end; padding-inline-end: 8px; white-space: nowrap;
}
.heatmap-cell {
  block-size: 26px; min-inline-size: 26px;
  border-radius: 4px; cursor: pointer;
  transition: transform var(--t-fast), box-shadow var(--t-fast);
}
.heatmap-cell.is-active {
  transform: scale(1.08);
  box-shadow: 0 0 0 2px var(--brand-700);
  z-index: 2; position: relative;
}

/* Histogram */
.histo-wrap { display: inline-block; inline-size: 100%; }
.histo {
  display: flex; align-items: flex-end; gap: 2px;
  inline-size: 100%; block-size: 26px;
}
.histo-bar {
  flex: 1; background: var(--brand-600);
  border-radius: 1px 1px 0 0;
  transition: filter var(--t-fast); min-block-size: 2px;
}
.histo-bar.is-active { filter: brightness(1.2) saturate(1.3); }

/* Gauge */
.gauge-wrap { display: inline-block; }
.gauge-track { fill: none; stroke: var(--bg-muted); stroke-width: 14; stroke-linecap: round; }
.gauge-band { fill: none; stroke-width: 14; stroke-linecap: butt; opacity: 0.18; }
.gauge-band.is-pass { stroke: var(--ok); }
.gauge-band.is-fail { stroke: var(--err); }
.gauge-band.is-warn { stroke: var(--warn); }
.gauge-band.is-brand { stroke: var(--brand-600); }
.gauge-indicator {
  fill: none; stroke: var(--brand-600); stroke-width: 14;
  stroke-linecap: round; transition: d var(--t-med);
}
.gauge-dot { fill: var(--brand-700); stroke: var(--bg-raised); stroke-width: 2; }
.gauge-value {
  font-size: var(--fsz-display); font-weight: 700; fill: var(--ink);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
  font-family: var(--font-ui);
}
.gauge-label {
  font-size: var(--fsz-caption); font-weight: 600; fill: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-family: var(--font-ui);
}
.veh-gauge-row { display: flex; align-items: center; gap: 14px; }
.veh-gauge-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; flex: 1; min-inline-size: 0; }
.veh-gauge-stat {
  display: flex; flex-direction: column;
  padding: 6px 10px; background: var(--bg-muted); border-radius: var(--r-sm);
}
.veh-gauge-stat-num {
  font-size: var(--fsz-h3); font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em; line-height: 1.2;
}
.veh-gauge-stat-label {
  font-size: var(--fsz-caption); color: var(--ink-3); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.veh-gauge-stat.is-pass .veh-gauge-stat-num { color: var(--ok); }
.veh-gauge-stat.is-fail .veh-gauge-stat-num { color: var(--err); }
@media (max-width: 480px) {
  .veh-gauge-row { flex-direction: column; align-items: stretch; }
  .veh-gauge-row .gauge-wrap { align-self: center; }
}

/* Funnel */
.funnel { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.funnel-row { display: flex; flex-direction: column; gap: 4px; }
.funnel-row-main {
  display: grid; grid-template-columns: 160px 1fr auto;
  align-items: center; gap: 14px;
}
@media (max-width: 720px) { .funnel-row-main { grid-template-columns: 100px 1fr auto; gap: 8px; } }
.funnel-label {
  font-size: var(--fsz-label); font-weight: 600; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .funnel-label { font-size: var(--fsz-body); }
.funnel-bar {
  block-size: 22px; background: var(--bg-muted);
  border-radius: var(--r-sm); overflow: hidden;
}
.funnel-fill {
  display: block; block-size: 100%;
  background: color-mix(in oklab, var(--brand-600) 35%, var(--bg));
  border-radius: inherit; transition: inline-size var(--t-med);
  position: relative;
}
.funnel-fill-pass {
  display: block; block-size: 100%; background: var(--brand-600);
  border-start-end-radius: 0; border-end-end-radius: 0;
}
.funnel-vals {
  display: flex; align-items: center; gap: 10px;
  min-inline-size: 110px; justify-content: flex-end;
}
.funnel-vol { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.funnel-pr {
  font-size: var(--fsz-label); padding: 2px 7px; border-radius: var(--r-pill);
  background: color-mix(in oklab, var(--ok) 18%, var(--bg)); color: var(--ok);
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.funnel-drop {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fsz-caption); color: var(--ink-3); font-weight: 600;
  align-self: center; margin-inline-start: 168px;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 720px) { .funnel-drop { margin-inline-start: 108px; } }
.funnel-drop span:first-child { color: var(--err); font-weight: 700; }

/* Inst view toggle */
/* .inst-view-toggle / .ivt-btn — retired. Use .seg-toggle.is-sm. */

/* ════════════════════════════════════════════════════════════════
   MapView — Dubai-style operational map with 3D bar markers
   ════════════════════════════════════════════════════════════════ */
.map-pro-wrap {
  display: flex; flex-direction: column;
  background: var(--bg-raised);
  border: 1px solid var(--line); border-radius: var(--r-md);
  overflow: hidden;
}
.map-pro-grid {
  display: grid; grid-template-columns: 1fr 280px;
  gap: 0; align-items: stretch;
}
@media (max-width: 1100px) { .map-pro-grid { grid-template-columns: 1fr; } }

.map-pro-map {
  position: relative;
  background: radial-gradient(ellipse at 30% 30%, color-mix(in oklab, var(--brand-700) 8%, var(--bg-raised)) 0%, var(--bg-raised) 70%);
  min-block-size: 460px;
}
[data-theme="dark"] .map-pro-map {
  background: radial-gradient(ellipse at 30% 30%, color-mix(in oklab, var(--brand-500) 14%, var(--bg-sunken)) 0%, var(--bg-sunken) 70%);
}
.map-pro-svg { display: block; inline-size: 100%; block-size: auto; aspect-ratio: 760 / 460; }

.map-bg-stop.is-inner { stop-color: var(--brand-tint); stop-opacity: 0.35; }
.map-bg-stop.is-outer { stop-color: var(--bg-raised); stop-opacity: 0; }
[data-theme="dark"] .map-bg-stop.is-inner { stop-color: var(--brand-500); stop-opacity: 0.10; }
[data-theme="dark"] .map-bg-stop.is-outer { stop-color: var(--bg-sunken); stop-opacity: 0; }

.map-pro-grid line { stroke: var(--line); stroke-width: 0.5; opacity: 0.35; }
[data-theme="dark"] .map-pro-grid line { opacity: 0.18; }

.map-pro-outline {
  fill: color-mix(in oklab, var(--brand-700) 6%, var(--bg-raised));
  stroke: color-mix(in oklab, var(--brand-700) 40%, var(--line-2));
  stroke-width: 1.5; stroke-linejoin: round; fill-opacity: 0.7;
}
[data-theme="dark"] .map-pro-outline {
  fill: color-mix(in oklab, var(--brand-500) 8%, var(--bg-muted));
  stroke: color-mix(in oklab, var(--brand-500) 40%, var(--line-2));
  fill-opacity: 0.5;
}

.map-compass-ring { fill: var(--bg-raised); stroke: var(--line-2); stroke-width: 1; }
.map-compass-needle { fill: var(--brand-600); }
.map-compass-label {
  fill: var(--ink-3); font-size: var(--fsz-chart-label); font-weight: 700;
  font-family: var(--font-ui); letter-spacing: 0.1em;
}

.map-zone-label {
  fill: var(--ink-3); font-size: var(--fsz-chart-label); font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase;
  font-family: var(--font-ui); pointer-events: none; opacity: 0.6;
}
[data-theme="dark"] .map-zone-label { opacity: 0.5; }

.map-bar { cursor: pointer; transition: filter var(--t-fast); }
.map-bar:hover, .map-bar.is-hover { filter: brightness(1.15) saturate(1.2); }

.map-bar-stop.is-good.is-top { stop-color: oklch(0.85 0.16 152); }
.map-bar-stop.is-good.is-mid { stop-color: oklch(0.62 0.18 150); }
.map-bar-stop.is-good.is-bot { stop-color: oklch(0.42 0.14 150); }
.map-bar-stop.is-mid.is-top  { stop-color: oklch(0.85 0.18  85); }
.map-bar-stop.is-mid.is-mid  { stop-color: oklch(0.65 0.16  60); }
.map-bar-stop.is-mid.is-bot  { stop-color: oklch(0.46 0.14  45); }
.map-bar-stop.is-bad.is-top  { stop-color: oklch(0.78 0.18  20); }
.map-bar-stop.is-bad.is-mid  { stop-color: oklch(0.58 0.20  18); }
.map-bar-stop.is-bad.is-bot  { stop-color: oklch(0.40 0.16  15); }

.map-bar.is-good .map-bar-side { fill: oklch(0.36 0.13 150); }
.map-bar.is-mid  .map-bar-side { fill: oklch(0.40 0.12  45); }
.map-bar.is-bad  .map-bar-side { fill: oklch(0.34 0.14  15); }
.map-bar.is-good .map-bar-cap { fill: oklch(0.92 0.10 150); }
.map-bar.is-mid  .map-bar-cap { fill: oklch(0.92 0.12  85); }
.map-bar.is-bad  .map-bar-cap { fill: oklch(0.90 0.14  20); }
.map-bar.is-good .map-bar-glow { fill: oklch(0.62 0.20 150); opacity: 0.5; }
.map-bar.is-mid  .map-bar-glow { fill: oklch(0.65 0.18  60); opacity: 0.55; }
.map-bar.is-bad  .map-bar-glow { fill: oklch(0.58 0.20  18); opacity: 0.55; }

.map-bar-base-dot { fill: var(--bg-raised); stroke: var(--ink-3); stroke-width: 1; }
.map-bar.is-good .map-bar-base-dot { stroke: oklch(0.50 0.18 150); }
.map-bar.is-mid  .map-bar-base-dot { stroke: oklch(0.55 0.18  60); }
.map-bar.is-bad  .map-bar-base-dot { stroke: oklch(0.50 0.20  18); }

.map-bar-mark {
  fill: var(--ink-2); font-size: var(--fsz-chart-label); font-weight: 700;
  font-family: var(--font-ui); letter-spacing: 0.04em;
  pointer-events: none;
}
.map-bar.is-hover .map-bar-mark { fill: var(--ink); }

.map-pro-legend-bg {
  fill: var(--bg-raised); stroke: var(--line); stroke-width: 1; fill-opacity: 0.92;
}
[data-theme="dark"] .map-pro-legend-bg { fill: var(--bg-muted); fill-opacity: 0.85; }
.map-pro-legend-title {
  fill: var(--ink-3); font-size: var(--fsz-chart-label); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  font-family: var(--font-ui);
}
.map-pro-legend-label {
  fill: var(--ink-2); font-size: var(--fsz-caption); font-weight: 500;
  font-family: var(--font-ui);
}
.map-pro-legend-bar.is-good { fill: oklch(0.55 0.16 150); }
.map-pro-legend-bar.is-mid  { fill: oklch(0.62 0.18  60); }
.map-pro-legend-bar.is-bad  { fill: oklch(0.55 0.20  18); }

.map-scrubber {
  display: flex; justify-content: center; gap: 4px;
  padding: 10px 14px 14px;
  border-block-start: 1px dashed var(--line);
  background: linear-gradient(180deg, transparent 0%, var(--bg-muted) 100%);
}
.map-scrub-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 14px; border: 1px solid transparent; background: transparent;
  color: var(--ink-3); border-radius: var(--r-pill);
  font: inherit; font-size: var(--fsz-label); font-weight: 500;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
/* RTL font-size bump removed — see comment on `.dash-range-btn`. */
.map-scrub-btn:hover { color: var(--ink); background: var(--bg-raised); }
.map-scrub-btn.is-on {
  background: var(--brand-600); color: var(--brand-ink);
  font-weight: 600; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-600) 18%, transparent);
}
.map-scrub-live {
  display: inline-block; inline-size: 6px; block-size: 6px;
  border-radius: 50%; background: var(--err);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--err) 30%, transparent);
}

.map-pro-side {
  border-inline-start: 1px solid var(--line);
  background: var(--bg-muted);
  display: flex; flex-direction: column; gap: 14px;
  padding: 16px;
}
@media (max-width: 1100px) {
  .map-pro-side { border-inline-start: none; border-block-start: 1px solid var(--line); }
}

.map-pro-side-stats {
  display: flex; flex-direction: column; gap: 10px;
  padding-block-end: 10px; border-block-end: 1px solid var(--line);
}
.map-pro-stat { display: flex; flex-direction: column; gap: 1px; }
.map-pro-stat-label {
  font-size: var(--fsz-caption); font-weight: 700; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.map-pro-stat-num {
  font-size: var(--fsz-h1); font-weight: 700; color: var(--ink);
  letter-spacing: -0.02em; line-height: 1.05; font-variant-numeric: tabular-nums;
}
.map-pro-stat-sub {
  display: flex; align-items: center; gap: 4px;
  font-size: var(--fsz-caption); color: var(--ink-3); margin-block-start: 2px;
}
.map-pro-stat-good { color: var(--ok); font-weight: 700; }

.map-pro-side-section { display: flex; flex-direction: column; gap: 8px; }
.map-pro-side-title {
  font-size: var(--fsz-caption); font-weight: 700; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.08em;
}

.map-pro-top5 { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.map-pro-top5-row {
  display: grid; grid-template-columns: 18px 22px 1fr auto;
  align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: var(--r-sm);
  cursor: pointer; transition: background var(--t-fast);
}
.map-pro-top5-row:hover { background: var(--bg-raised); }
.map-pro-top5-rank { font-size: var(--fsz-caption); font-weight: 700; color: var(--ink-3); text-align: center; font-variant-numeric: tabular-nums; }
.map-pro-top5-row:nth-child(1) .map-pro-top5-rank { color: oklch(0.62 0.18 85); }
.map-pro-top5-row:nth-child(2) .map-pro-top5-rank { color: oklch(0.65 0.04 280); }
.map-pro-top5-row:nth-child(3) .map-pro-top5-rank { color: oklch(0.55 0.10 50); }
.map-pro-top5-mark {
  inline-size: 22px; block-size: 22px;
  border-radius: var(--r-sm); display: grid; place-items: center;
  font-size: var(--fsz-chart-label); font-weight: 700; color: #fff; background: var(--brand-600);
}
.map-pro-top5-mark[data-tone="A"] { background: oklch(0.55 0.16 240); }
.map-pro-top5-mark[data-tone="B"] { background: oklch(0.58 0.17 18); }
.map-pro-top5-mark[data-tone="C"] { background: oklch(0.55 0.12 160); }
.map-pro-top5-mark[data-tone="D"] { background: oklch(0.55 0.15 285); }
.map-pro-top5-mark[data-tone="E"] { background: oklch(0.55 0.14 205); }
.map-pro-top5-mark[data-tone="F"] { background: oklch(0.60 0.14 45); }
.map-pro-top5-mark[data-tone="G"] { background: oklch(0.55 0.13 325); }
.map-pro-top5-mark[data-tone="H"] { background: oklch(0.55 0.12 185); }
.map-pro-top5-mark[data-tone="I"] { background: oklch(0.55 0.11 120); }
.map-pro-top5-mark[data-tone="J"] { background: oklch(0.55 0.15 255); }

.map-pro-top5-name { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }
.map-pro-top5-name-en {
  font-size: var(--fsz-label); font-weight: 600; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[dir="rtl"] .map-pro-top5-name-en { font-size: var(--fsz-body); }
.map-pro-top5-meta { font-size: var(--fsz-caption); color: var(--ink-3); font-variant-numeric: tabular-nums; }
.map-pro-top5-pr {
  font-size: var(--fsz-label); font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 2px 7px; border-radius: var(--r-pill);
}
.map-pro-top5-pr.is-good { background: color-mix(in oklab, var(--ok) 16%, var(--bg)); color: var(--ok); }
.map-pro-top5-pr.is-mid  { background: color-mix(in oklab, var(--warn) 16%, var(--bg)); color: var(--warn); }
.map-pro-top5-pr.is-bad  { background: color-mix(in oklab, var(--err) 16%, var(--bg)); color: var(--err); }

/* ════════════════════════════════════════════════════════════════
   DASHBOARD — Reference-fidelity design system
   ════════════════════════════════════════════════════════════════
   Tokens, type scale, shadow scale, and semantic colours scoped to
   the dashboard surface. Lives BELOW the legacy dashboard styles so
   it overrides where keys collide. Mobility dashboard is unaffected.
*/
.app:has(.dash-header) {
  /* ── Light mode (default) ── */
  --d-bg-page:    oklch(0.975 0.003 250);
  --d-bg-card:    #ffffff;
  --d-line:       oklch(0.92 0.005 250);
  --d-line-soft:  oklch(0.95 0.004 250);
  --d-ink:        oklch(0.18 0.02 250);
  --d-ink-2:      oklch(0.34 0.015 250);
  --d-ink-3:      oklch(0.52 0.012 250);
  --d-ink-4:      oklch(0.70 0.008 250);
  --d-ok:         oklch(0.62 0.18 145);
  --d-ok-soft:    oklch(0.96 0.05 145);
  --d-err:        oklch(0.60 0.22 25);
  --d-err-soft:   oklch(0.96 0.06 25);
  --d-warn:       oklch(0.74 0.16 60);
  --d-warn-soft:  oklch(0.97 0.05 60);
  --d-indigo:     oklch(0.55 0.18 268);
  --d-indigo-soft:oklch(0.96 0.04 268);
  --d-purple:     oklch(0.55 0.18 290);
  --d-male:       oklch(0.55 0.16 240);
  --d-female:     oklch(0.62 0.20 350);
  --d-sh-card:    0 1px 0 0 rgba(15, 17, 21, 0.04), 0 1px 2px 0 rgba(15, 17, 21, 0.04);
  --d-sh-card-hover: 0 1px 0 0 rgba(15, 17, 21, 0.06), 0 4px 14px -4px rgba(15, 17, 21, 0.10);
  /* --d-r-card and --d-r-pill removed — duplicates of --r-lg / --r-pill.
     Use the canonical token-scale tokens directly. */
  /* "Device surface" — used by the digital-clock timer block, code chips,
     "Open" button, and the live-test step pill. Charcoal in light mode for
     a high-contrast device-y look; in dark mode it lifts to an elevated
     card surface so it doesn't disappear into the page. */
  --d-device:     oklch(0.18 0.02 250);
  --d-device-fg:  white;
  --d-device-mute:oklch(0.70 0.01 250);
  --d-device-bar: oklch(0.32 0.02 250);
  --d-device-pill:oklch(0.25 0.02 250);
}

/* ── Dark mode override ── all token values flip together so cards,
     surfaces, ink, lines, and semantic-soft variants stay readable when
     `[data-theme="dark"]` is applied at the document root. */
[data-theme="dark"] .app:has(.dash-header),
[data-theme="dark"] body:has(.dash-header) {
  /* Page + card surfaces matched to the canvas's dark-mode bg
     tokens (tokens.css ~L483: --bg = 0.255, --bg-raised = 0.295)
     so dashboards no longer read as near-black while the rest of
     the system sits at medium-dark grey. Brand-hue chroma also
     unified with `var(--brand-h)` so dark surfaces inherit subtle
     brand tinting just like the canvas — keeps the whole dark-mode
     palette in one family across pages.
     Was: 0.16 / 0.21 with hardcoded 250 hue — visibly darker. */
  --d-bg-page:    oklch(0.255 0.001 var(--brand-h));
  --d-bg-card:    oklch(0.295 0.002 var(--brand-h));
  --d-line:       oklch(0.30 0.008 250);
  --d-line-soft:  oklch(0.25 0.006 250);
  --d-ink:        oklch(0.96 0.005 250);
  --d-ink-2:      oklch(0.82 0.006 250);
  --d-ink-3:      oklch(0.62 0.008 250);
  --d-ink-4:      oklch(0.46 0.008 250);
  --d-ok:         oklch(0.74 0.16 145);
  --d-ok-soft:    oklch(0.30 0.06 145);
  --d-err:        oklch(0.72 0.20 25);
  --d-err-soft:   oklch(0.32 0.08 25);
  --d-warn:       oklch(0.80 0.15 60);
  --d-warn-soft:  oklch(0.32 0.07 60);
  --d-indigo:     oklch(0.70 0.18 268);
  --d-indigo-soft:oklch(0.28 0.07 268);
  --d-purple:     oklch(0.72 0.18 290);
  --d-male:       oklch(0.68 0.16 240);
  --d-female:     oklch(0.74 0.20 350);
  --d-sh-card:    0 1px 0 0 rgba(255, 255, 255, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.4);
  --d-sh-card-hover: 0 1px 0 0 rgba(255, 255, 255, 0.06), 0 4px 14px -4px rgba(0, 0, 0, 0.5);
  /* Device surface: lift above page bg so it doesn't disappear */
  --d-device:      oklch(0.13 0.005 250);
  --d-device-fg:   oklch(0.96 0.005 250);
  --d-device-mute: oklch(0.62 0.008 250);
  --d-device-bar:  oklch(0.26 0.008 250);
  --d-device-pill: oklch(0.20 0.008 250);
}

/* Page background — Tier-1 dashboard inherits the base `.main`
   tinted-gray bg (no canvas wrapper), and the DashHead at L1 has
   its card chrome stripped (`.is-bare`) so it sits transparently
   on top of the page bg. Tier-2 / Tier-3 keep the white-tile
   chrome on their DashHead since they need the visual "page
   header" anchor. */
.dash-header.is-bare {
  background: transparent;
  border: 0;
  box-shadow: none;
  padding-inline: 0;
  /* Top padding zeroed — `.main--dash`'s 16px is the page's top
     gap; the bare-variant has no card chrome that needs internal
     breathing space. Matches the gap on Saved Tests / Reports. */
  padding-block: 0 8px;
}
/* The time range now uses `.seg-toggle.is-md`, so its styling
   inherits from the canonical segmented-control rules above —
   no `.is-bare` override needed. Single source of truth. */

/* ── LevelChain dashboard variant — used by DashHead ────────────
   Reuses the `.rpt-level-chain` markup from Reports so the
   drill-down navigation looks the same across the system. Differences
   from Reports' usage:
     - L1 / L2 / L3 number badge suppressed (dashboards have only 3
       levels with self-explanatory labels — numbering reads as
       ceremony).
     - Tighter padding so it sits cleanly inside the slimmer header.
     - Hint hidden (Reports needs it for the deep-drill discovery
       moment; on dashboards the chain is always visible). */
.rpt-level-chain-wrap.is-dash-variant {
  /* Override the base wrap's column flex so the eyebrow + chain
     pills sit side-by-side on one row, left-aligned to the page
     leading edge. */
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: 0;
}
.rpt-level-chain-wrap.is-dash-variant .rpt-level-num { display: none; }
.rpt-level-chain-wrap.is-dash-variant .rpt-level-pill-inner {
  padding: 2px 9px;
  font-size: var(--fsz-label);
}
/* Eyebrow inline variant — renders as the leading prefix of the
   level-chain breadcrumb on T2/T3 dashboards. Static orientation
   chrome: keeps the uppercase + tracking + ink-3 + no-hover
   treatment so the eye reads it as "module context" rather than
   a clickable level pill. Vertical separator after the eyebrow
   visually divides static orientation from interactive navigation. */
.rpt-level-chain-wrap.is-dash-variant .page-h1-eyebrow.is-inline {
  margin-block-end: 0;
  margin-inline-end: 10px;
  padding-inline-end: 12px;
  border-inline-end: 1px solid var(--line);
  /* Slightly compress to fit the chain row's natural height */
  padding-block: 2px;
}
.rpt-level-chain-wrap.is-dash-variant .rpt-level-hint {
  display: none;
}

/* ── DashHeader — page chrome card ─────────────────────────────── */
.dash-header {
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: relative;
  box-shadow: var(--d-sh-card);
}
.dash-header-crumb {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-label);
  color: var(--d-ink-3);
}
.dash-header-crumb-link {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
  border-radius: 4px;
  padding: 1px 4px;
  transition: color 120ms ease, background 120ms ease;
}
.dash-header-crumb-link:hover { color: var(--d-ink); background: var(--d-line-soft); }
.dash-header-crumb-current { color: var(--d-ink); font-weight: 600; }
.dash-header-crumb-sep { color: var(--d-ink-4); font-size: var(--fsz-caption); }

.dash-header-row {
  display: flex;
  align-items: center;
  gap: 16px;
  position: relative;
}
/* `.dash-header-accent` retired — the colored left bar was visual
   chrome that the user found distracting. The class is left out so any
   stale markup with the class becomes a no-op div. */

/* ── LiveBadge ── pulsing-dot pill used wherever streaming-data state
   is surfaced. The container is intentionally neutral (gray text on
   the page-line tint) — the only red element is the small pulsing
   dot. This mirrors the iOS / TV-broadcast convention ("REC" pill)
   and avoids the colour collision that the previous all-red pill had
   with red KPI delta pills nearby. */
/* LiveBadge migrated to the master `.pill` primitive:
   - Wrapper: `.pill.is-neutral.is-uppercase` (recessed neutral chip, fits L2/L3 white-card headers)
   - Dot:     `.pill-dot.is-err.has-pulse` (red, animated)
   The `.dash-live-badge` class is kept on the wrapper purely as an
   addressing hook for two responsive helpers (`--inline` / `--meta`)
   and the L1 contrast override below. All visual styling now flows
   from `.pill` rules above. */

/* L1 contrast inversion — on the bare-header tier the LIVE pill sits
   on the SUNKEN page bg, so it needs to be ELEVATED (white/paper)
   instead of recessed. Reuses the `.is-paper` recipe in-place rather
   than asking the LiveBadge component to know which tier it's on. */
.dash-header.is-bare .pill.dash-live-badge.is-neutral {
  background: var(--bg-raised);
  color: var(--ink-2);
  border-color: var(--line);
}
[data-theme="dark"] .dash-header.is-bare .pill.dash-live-badge.is-neutral {
  background: oklch(0.32 0.010 var(--brand-h));
  color: var(--ink-2);
  border-color: oklch(0.40 0.014 var(--brand-h));
}

/* `.dash-live-badge--inline` (default) sits inline with the title;
   `--meta` is a hidden duplicate that lives at the start of the meta
   row and swaps in at ≤540px. Default desktop: meta copy hidden. */
.dash-live-badge--meta { display: none; }

/* ── DashHint ── single-line affordance hint, matches the visual
   language of `.rpt-level-hint` from Reports (text + icon, no
   container background). Replaces the old brand-tinted `.dash-hint`
   pill which read as a banner, not a hint. */
.dash-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  white-space: nowrap;
  margin-block-end: 8px;
}
.dash-hint svg { color: var(--d-indigo); flex-shrink: 0; }
[dir="rtl"] .dash-hint { font-size: var(--fsz-label); }

/* ── Affordance row ── one horizontal line: hint on the start side,
   tools (view-toggle, etc.) pinned on the end side. Replaces a
   separate `branch-deep-head` block for the hint + toggle pairing. */
.dash-affordance-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.dash-affordance-row .dash-hint { margin-block-end: 0; }

/* `.branch-deep.is-bare` (legacy) — strips the wrapper card chrome.
   No longer used on Tier 1 (we reverted to the framed container) but
   kept around in case other callers want a no-chrome variant. */
.branch-deep.is-bare {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
}

/* Affordance row pinned inside a section's `.branch-deep` container —
   sits above the body content (grid / table / map). The bottom margin
   is taken care of by `.branch-deep`'s `gap: 12px`, so we suppress the
   nested `.dash-hint`'s own margin and rely on the parent rhythm. */
.dash-affordance-row--in-section { margin-block-end: 0; }

/* Inline letter avatar inside table name cells — same colour-tone
   palette as the InstituteCard tiles, scaled down for table density. */
.cell-inst-name {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* Two-line stack: institute name on top, "X branches" subtitle below.
   Mirrors the InstituteCard's name + location subtitle composition. */
.cell-inst-text {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  min-inline-size: 0;
}
.cell-inst-title {
  font-weight: 600;
  color: var(--tbl-cell-strong);
  font-size: var(--fsz-body);
  line-height: 1.2;
}
.cell-inst-sub {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--ink-3);
  line-height: 1.2;
}
.cell-inst-sub > svg { color: var(--ink-4); flex-shrink: 0; }
.cell-inst-tile {
  /* 45×28 (1.6:1 landscape — matches the L1 card-view tile and the
     L2 `.dash-header-icon` ratio). Same visual language across all
     three surfaces. Initials-fallback chip fills the box; the logo
     variant gets 2px internal padding so the mark breathes inside
     the canvas. */
  inline-size: 45px;
  block-size: 28px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  color: white;
  font-weight: 700;
  font-size: var(--fsz-caption);
  letter-spacing: 0.04em;
  flex-shrink: 0;
}
.cell-inst-tile[data-tone="A"] { background: oklch(0.55 0.20 290); }
.cell-inst-tile[data-tone="B"] { background: oklch(0.60 0.16 50);  }
.cell-inst-tile[data-tone="C"] { background: oklch(0.62 0.15 220); }
.cell-inst-tile[data-tone="D"] { background: oklch(0.62 0.18 158); }
.cell-inst-tile[data-tone="E"] { background: oklch(0.55 0.22 350); }
.cell-inst-tile[data-tone="F"] { background: oklch(0.62 0.16 195); }
.cell-inst-tile[data-tone="G"] { background: oklch(0.58 0.19 18);  }
.cell-inst-tile[data-tone="H"] { background: oklch(0.65 0.18 95);  }
/* Logo variant — borderless transparent frame so the logo art reads
   directly without a chrome ring. Tiny scale (26px tile) made the
   border feel like noise. The double-class selector overrides any
   data-tone background colour above. */
.cell-inst-tile.cell-inst-tile--logo {
  background: transparent;
  border: none;
  padding: 2px;
}
.cell-inst-tile--logo > img {
  /* No padding — logo fills the full 24×32 tile, with object-fit:
     contain preserving aspect against the box. */
  inline-size: 100%;
  block-size: 100%;
  /* Override grid item's default min-block-size: auto so SVG
     intrinsic height (e.g. EDI 48) doesn't push the img past the
     parent's 24px. */
  min-block-size: 0;
  object-fit: contain;
  display: block;
}
[data-theme="dark"] .cell-inst-tile.cell-inst-tile--logo {
  /* Dark-mode logo canvas — near-white (94% L) so colourful brand
     marks stay legible against the dark row background. A subtle
     border softens the transition between the canvas and the row. */
  background: oklch(0.94 0 0);
  border: 1px solid color-mix(in oklab, var(--line) 50%, transparent);
}
.dash-header-icon {
  /* 60×40 slot (1.5:1 landscape — narrower than the previous 64×40
     and centered both axes so the logo no longer hugs the title
     edge). Chrome stripped so the logo reads as a freestanding
     mark, not a framed tile. */
  inline-size: 60px;
  block-size: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--d-indigo);
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
/* Letter fallback — used when an institute has no icon image and
   no SVG glyph; we render a styled letter (1-2 chars) inside the
   icon slot. Replaces an inline JSX style block. */
.dash-header-icon-letter {
  font-size: var(--fsz-body);
  font-weight: 700;
  letter-spacing: 0.4px;
}
/* Image variant inherits the chrome-less treatment — the institute
   logo renders directly on the page background. */
.dash-header-icon.is-image {
  background: transparent;
  color: var(--d-ink);
  border: 0;
}
[data-theme="dark"] .dash-header-icon.is-image {
  /* Header logo lockup (wordmark) needs a near-white canvas in dark
     mode so colourful brand marks stay legible. Padded a touch so
     the wordmark doesn't touch the canvas edges. */
  background: oklch(0.94 0 0);
  border: 1px solid color-mix(in oklab, var(--line) 50%, transparent);
  border-radius: var(--r-md);
  padding: 4px 8px;
}
.dash-header-icon.is-image > img {
  /* Logo fills the 60×40 slot and is centered both axes —
     object-fit: contain preserves the natural aspect ratio so wide
     wordmark lockups (BDC 2.32:1, DD 4.36:1) letterbox vertically
     and square marks letterbox horizontally inside the frame. */
  block-size: 100%;
  inline-size: 100%;
  max-inline-size: 100%;
  /* Override flex item's min-block-size: auto so the img doesn't
     keep its intrinsic SVG height (e.g. EDI's 48px) and instead
     shrinks to the parent's 40px. */
  min-block-size: 0;
  object-fit: contain;
  object-position: center;
  display: block;
}
.dash-header-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.dash-header-tier { display: none; } /* Tier label dropped — breadcrumb already locates the page */
.dash-header-title-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.dash-header-title {
  margin: 0;
  font-size: var(--fsz-h1);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--d-ink);
  line-height: 1.15;
}
.dash-header-switch {
  appearance: none;
  border: 1px solid var(--d-line);
  background: transparent;
  color: var(--d-ink-3);
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  transition: all 120ms ease;
}
.dash-header-switch:hover { border-color: var(--d-indigo); color: var(--d-indigo); background: var(--d-indigo-soft); }
.dash-header-meta {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  font-size: var(--fsz-label);
  color: var(--d-ink-3);
  padding-block-start: 2px;
}
.dash-header-meta-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.dash-header-meta-item b { color: var(--d-ink-2); font-weight: 600; }
.dash-header-meta-sep {
  color: var(--d-ink-4);
}
.dash-header-status {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--d-ok);
  padding: 2px 8px 2px 6px;
  background: var(--d-ok-soft);
  border-radius: var(--r-pill);
}
.dash-header-status::before {
  content: "";
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: var(--d-ok);
}
.dash-header-status.is-warn { color: var(--d-warn); background: var(--d-warn-soft); }
.dash-header-status.is-warn::before { background: var(--d-warn); }
.dash-header-status.is-err { color: var(--d-err); background: var(--d-err-soft); }
.dash-header-status.is-err::before { background: var(--d-err); }

.dash-header-tools {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
/* Range picker → short labels at narrow widths only.
   ────────────────────────────────────────────────────────────
   Trigger: `@media (max-width: 480px)`. Picked because:
     - Full picker is ~370px natural width at .is-md size
     - Viewport chrome (body padding, header padding) eats ~58px
     - So row inline-size = viewport - ~58
     - Picker fits when row ≥ 370, i.e., viewport ≥ ~430
     - 480 gives a comfortable buffer above that threshold
   ────────────────────────────────────────────────────────────
   Why 480 (NOT 720): the 720 breakpoint wraps `.dash-header-tools`
   onto its own line, but tablets at 600–720 have hundreds of px
   to spare for the picker. Swapping to short labels at 720 was
   wrong because the picker fits comfortably at those widths.
   Shorts should only kick in at narrow phone portrait widths.
   ────────────────────────────────────────────────────────────
   A container query on `.dash-header-tools` would be cleaner
   (width-based vs viewport-based) BUT `container-type: inline-size`
   on a flex item that's sized by content collapses it to zero,
   which then mis-fires the query at every viewport. The codebase's
   .rpt-head-row uses container-type on a flex CONTAINER with
   parent-derived size — different pattern, doesn't apply here. */
@media (max-width: 480px) {
  .dash-header-tools .seg-toggle.is-md { scrollbar-width: none; }
  .dash-header-tools .seg-toggle.is-md::-webkit-scrollbar { display: none; }
  .dash-header-tools .seg-toggle.is-md .seg-toggle-label:not(.seg-toggle-label--short) { display: none; }
  .dash-header-tools .seg-toggle.is-md .seg-toggle-label--short { display: inline; }
}
/* `.dash-header-range*` legacy classes removed — the time-
   range picker now uses the canonical `.seg-toggle.is-md` along
   with every other segmented control in the app, so any change
   to active-state styling lands once and propagates everywhere. */
/* ≤540px — meta row breaks out below the icon and flows
   inline with the LIVE badge so [LIVE · examiners 6 · vehicles 5]
   share a single row instead of stacking. Uses `display: contents`
   on `.dash-header-info` to let title-row and meta-row become
   direct grid children of `.dash-header-row`, then grid-places
   the meta row at `1 / -1` so it spans full width below the icon. */
@media (max-width: 540px) {
  .dash-header-row {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 12px;
    row-gap: 6px;
    align-items: start;
    flex-wrap: initial;
  }
  .dash-header-icon { grid-column: 1; grid-row: 1; }
  .dash-header-info { display: contents; }
  .dash-header-title-row { grid-column: 2; grid-row: 1; }
  .dash-header-meta { grid-column: 1 / -1; grid-row: 2; }
  .dash-header-tools { grid-column: 1 / -1; grid-row: 3; }
  /* Swap which LIVE badge is visible */
  .dash-live-badge--inline { display: none; }
  .dash-live-badge--meta {
    display: inline-flex;
    /* Render LIVE last visually so the row reads as
       `meta items → LIVE`. `order: 99` reorders without changing
       the DOM (LIVE is still the first child of the meta wrapper),
       and LIVE flows after the meta items with the row's natural
       `gap` — not pinned to the trailing edge. */
    order: 99;
  }
}
@media (max-width: 720px) {
  .dash-header-row { flex-wrap: wrap; }
  /* On mobile the tools row wraps to its own line under the title.
     `flex-shrink: 1` + `min-inline-size: 0` lets the tools shrink
     to the parent row's actual width — without them, the base
     rule's `flex-shrink: 0` combined with `min-width: auto`
     (= sum of children's intrinsic widths) blew past the row
     width. The short labels below shave the picker to ~250px so
     internal scroll never has to engage. */
  .dash-header-tools {
    order: 3;
    flex-basis: 100%;
    flex-shrink: 1;
    min-inline-size: 0;
  }
  .dash-header-title { font-size: var(--fsz-h2); }
  /* NOTE: the range-picker label swap previously lived here at
     `@media (max-width: 720px)`. It moved up to a `@container
     dash-header-tools (max-width: 380px)` block (next to the
     .dash-header-tools rule) so the swap fires based on the
     ACTUAL tools width — not just the breakpoint where tools
     wraps onto its own line — which means full labels stay on
     wider tablets where the row has plenty of room even after
     wrapping. */
}
/* Desktop default: full label visible, short hidden. The container
   query above re-enables `--short` only when the tools row is
   narrower than the picker's natural width (~370px). */
.seg-toggle-label--short { display: none; }

/* ── Canonical segmented toggle ──────────────────────────────────
   Single component, four sizes. Used for any "pick one of N"
   surface (range pickers, view switchers, mode toggles, prefs).

   Shape: ALL interactive controls (toggles / buttons / selections)
   share the same soft rounded shape (var(--r-md) / var(--r-sm)).
   Pill (999px) is reserved for non-interactive chips & status
   badges. Using pill on toggles makes them read like display
   chips and blurs the difference between "click target" and
   "label" — so we don't.

   Variants are size-only — radius is uniform across is-md/is-sm/
   is-compact. The icon variant is the one exception (22×22 micro
   gets a tighter 4px button radius because soft 8px on a 22×22
   square eats into the icon).

   - .is-md      range picker            (12.5px / 5×14)
   - .is-sm      view & mode toggles     (11.5px / 4×10)
   - .is-compact prefs / utilities       (12px   / 6×12)
   - .is-icon    22×22 icon-only         (tight square)

   Active state: white card on neutral container with a 1px shadow.
   The legacy brand-tint active state (used by .inst-view-toggle /
   .man-mode-toggle / .wvt-btn in V1) is deliberately retired —
   it reads as a permanent "selected mode" badge against the$
   neutral page, not a quiet segmented control.                   */
.seg-toggle {
  display: inline-flex;
  border: 1px solid var(--d-line, var(--line));
  border-radius: var(--r-md, 10px);
  padding: 2px;
  background: var(--d-bg-page, var(--bg-sunken));
  gap: 0;
  /* On narrow viewports (≤480px) the chips can collectively exceed
     the available row width. Allow horizontal scroll inside the
     toggle so the chips stay on one line + remain reachable instead
     of overflowing the page header. max-inline-size caps the
     control at the viewport edge. */
  max-inline-size: 100%;
  overflow-x: auto;
  scrollbar-width: thin;
}
.seg-toggle::-webkit-scrollbar { block-size: 4px; }
.seg-toggle-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--d-ink-3, var(--ink-3));
  font: inherit;
  font-weight: 500;
  border-radius: var(--r-sm, 8px);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  white-space: nowrap;
  transition: color 120ms ease, background 120ms ease, box-shadow 120ms ease;
}
.seg-toggle-btn:hover { color: var(--d-ink, var(--ink)); }
.seg-toggle-btn.is-on {
  background: var(--d-bg-card, var(--bg-raised));
  color: var(--d-ink, var(--ink));
  /* Outlined active state — single source of truth for ALL
     segmented controls (range picker, view toggle, language /
     mode pickers, density toggle, direction toggle). 1px hairline
     ring inside the pill plus a soft 1px drop-shadow lifts the
     active state off the track without colored fill. */
  box-shadow:
    inset 0 0 0 1px var(--d-line, var(--line)),
    var(--d-sh-card, var(--sh-xs));
}
/* Live dot — small red pulsing indicator that can sit inside any
   active seg-toggle button to signal real-time data (currently
   used by the time-range picker's "Today" pill). Same color +
   keyframe as `.dash-live-badge-dot` so both live indicators
   read as the same component. */
/* `.seg-toggle-live` styling MIGRATED to `.pill-dot.is-err.has-pulse`.
   Class name kept on the markup as addressing hook; no styling here. */

/* Size variants — radius scales with size to keep the visual weight
   proportional. .is-md is the base (10/8); .is-sm and .is-compact
   step down to 8/6 because at ~30px tall, 10px radius reads as too
   bubbly. .is-icon stays at 6/4 for the same reason at 22×22.
   Track padding scales with size: .is-md gets 3px (matches the
   original `.dash-header-range` proportions); the smaller
   variants keep 2px. */
.seg-toggle.is-md                         { padding: 3px; }
.seg-toggle.is-md > .seg-toggle-btn       { font-size: var(--fsz-label); padding: 5px 14px; }

.seg-toggle.is-sm                         { border-radius: var(--r-sm); }
/* Explicit line-height on the is-sm button — without it, Arabic
   content ("العربية") inherits a taller intrinsic line-height than
   Latin ("Cards" / "List"), so a language toggle sits visibly taller
   than an adjacent view toggle even though both share the same
   class. Pinning to 1.2 normalises the button height regardless of
   script. */
.seg-toggle.is-sm > .seg-toggle-btn       { font-size: var(--fsz-caption); padding: 4px 10px; border-radius: var(--r-xs); line-height: 1.2; }

.seg-toggle.is-compact                    { border-radius: var(--r-sm); }
.seg-toggle.is-compact > .seg-toggle-btn  { font-size: var(--fsz-label);   padding: 6px 12px; gap: 6px; border-radius: var(--r-xs); }

/* Icon micro — tight square; soft radius on 22×22 buttons eats the icon. */
.seg-toggle.is-icon                       { border-radius: var(--r-xs); }
.seg-toggle.is-icon > .seg-toggle-btn {
  inline-size: 22px;
  block-size: 22px;
  padding: 0;
  border-radius: 4px;
}

/* .is-stretch — grid layout that stretches columns equally. Combine
   with any size variant. Used in narrow drawers / dropdowns where
   buttons should fill the row (ProfileMenu language picker, theme
   picker, BrandingPanel density). */
.seg-toggle.is-stretch {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
}
.seg-toggle.is-stretch > .seg-toggle-btn {
  inline-size: 100%;
  justify-content: center;
}

/* RTL — slight font bump for Arabic readability, mirroring the
   pattern used elsewhere on small UI text. */
[dir="rtl"] .seg-toggle.is-md > .seg-toggle-btn  { font-size: var(--fsz-body); }
[dir="rtl"] .seg-toggle.is-sm > .seg-toggle-btn  { font-size: var(--fsz-label); }
[dir="rtl"] .seg-toggle.is-compact > .seg-toggle-btn { font-size: var(--fsz-label); }

/* ── KPI STRIP — Tier-1 dashboard ────────────────────────────
   Scoped via `:has(.kpi-card)` so this only matches the Tier-1
   KPI strip (4 cards with embedded charts that use `.kpi-card`).
   The savedtests + reports KPI strip uses `.kpi` cards and keeps
   its base auto-fit rule defined earlier in this file. */
.app:has(.kpi-strip) .org-strip { display: none; }   /* hide legacy strip */
.kpi-strip:has(.kpi-card) {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1.6fr;
  gap: 14px;
  /* No margin-top — the gap to the previous DashHead now comes from
     the parent container's natural gap, matching the gap to the
     section below (was 34px above / 18px below — asymmetric). */
}
@media (max-width: 1279px) {
  .kpi-strip:has(.kpi-card) { grid-template-columns: 1fr 1fr; }
}
.kpi-card {
  /* `position: relative` so the absolute-positioned hover popover
     (.kpi-card-pop) anchors to this card. Without it the popover
     falls back to the viewport as containing block and renders at
     viewport-bottom + 4px — i.e. completely outside the card. */
  position: relative;
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  padding: 10px 14px 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  box-shadow: var(--d-sh-card);
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.kpi-card:hover {
  border-color: color-mix(in oklab, var(--d-indigo) 35%, var(--d-line));
  box-shadow: var(--d-sh-card-hover);
}
.kpi-card-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 22px;
}
.kpi-card-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--d-ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.kpi-card-num {
  font-size: var(--fsz-display);
  font-weight: 700;
  color: var(--d-ink);
  letter-spacing: -0.02em;
  line-height: 1.05;
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-variant-numeric: tabular-nums;
}
.kpi-card-num-suffix {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--d-ink-3);
  letter-spacing: normal;
}
.kpi-card-chart {
  block-size: 50px;
  position: relative;
}
/* Mobility-style: number + small inline sparkline share a row, with
   the chart taking ~120px on the right instead of a full-width band
   below the value. Calmer and more dashboard-like. */
.kpi-card-num-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 38px;
}
.kpi-card-chart--inline {
  block-size: 38px;
  inline-size: 120px;
  flex-shrink: 0;
  align-self: center;
}
.kpi-card-chart--inline .kpi-spark { inline-size: 100%; block-size: 100%; }

/* ── KPI mobility-style card (used by KpiTodayTestsCard) ──
   Big number stands alone; the foot row hosts the comparison sub-text
   on the left and the small sparkline on the bottom-right. The
   pass/fail/absent breakdown moves to a hover popover. */
.kpi-card--mobility {
  position: relative;
  cursor: default;
}
.kpi-card-foot--mobility {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
  padding-block-start: 4px;
}
.kpi-card-chart--mobility {
  block-size: 32px;
  inline-size: 110px;
  flex-shrink: 0;
}
.kpi-card-chart--mobility .kpi-spark { inline-size: 100%; block-size: 100%; }

/* Hover breakdown popover — anchored below the card.
   Content-sized (not stretched), 3 rows for pass/fail/absent. */
/* KpiTodayTestsCard breakdown — INNER content only.
   Outer chrome provided by the wrapping <ChartTip> portal. */
.kpi-card-pop {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-inline-size: 200px;
}
.kpi-card-pop-row {
  display: grid;
  grid-template-columns: 8px 1fr auto;
  align-items: center;
  gap: 8px;
  font-variant-numeric: tabular-nums;
}
.kpi-card-pop-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
}
.kpi-card-pop-row.is-pass   .kpi-card-pop-dot { background: var(--pass); }
.kpi-card-pop-row.is-fail   .kpi-card-pop-dot { background: var(--fail); }
.kpi-card-pop-row.is-absent .kpi-card-pop-dot { background: var(--absent); }
.kpi-card-pop-label { color: var(--tooltip-fg-muted); font-size: var(--fsz-caption); }
.kpi-card-pop-num   { color: var(--tooltip-fg); font-weight: 600; font-size: var(--fsz-label); }
.kpi-card-bigchart {
  /* Negative inline margin extends the chart full-bleed into the
     card's horizontal padding so it reaches edge-to-edge (less
     dead-space on left/right). Block-size 54px → contributes ~124px
     total card height. */
  block-size: 54px;
  position: relative;
  margin-block: 2px;
  margin-inline: -8px;
}
.kpi-card-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  padding-block-start: 2px;
  /* Pin foot to the bottom of the card — when card height is larger
     than content (driven by the wide chart card 4), the foot sits at
     the bottom and the gap between num and foot expands to fill. */
  margin-block-start: auto;
}
.kpi-card-foot--segs { gap: 12px; flex-wrap: wrap; }
.kpi-card-foot--split { justify-content: space-between; }
.kpi-card-foot-label b { color: var(--d-ink); font-weight: 600; }
.kpi-seg {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
}
.kpi-seg b, .kpi-seg .tnum {
  color: var(--d-ink-3);
  font-weight: 600;
}
.kpi-seg-dot {
  inline-size: 7px;
  block-size: 7px;
  border-radius: 50%;
  display: inline-block;
}
/* Pass / Fail / Absent — use canonical savedtests pill tokens so the
   colour story is one system across the app: green pass, red fail,
   neutral-gray absent. The previous `is-warn` (orange) for absent was
   a invention that broke consistency with the saved-tests pill. */
.kpi-seg-dot.is-pass   { background: var(--pass); }
.kpi-seg-dot.is-fail   { background: var(--fail); }
.kpi-seg-dot.is-absent { background: var(--absent); border: 1px solid var(--absent-border); box-sizing: border-box; }
/* Aliases for older tone names — keep so we don't break other callers */
.kpi-seg-dot.is-ok   { background: var(--pass); }
.kpi-seg-dot.is-err  { background: var(--fail); }
.kpi-seg-dot.is-warn { background: var(--absent); border: 1px solid var(--absent-border); box-sizing: border-box; }

/* Tier-1 rail also gets the dashboard chrome scope so the
   header / shell tokens cascade correctly (tokens are gated to
   `.app:has(.dash-header)` etc.). */
.d2-side--kpi {
  /* The default `.d2-side` is `position: sticky; top: 12px`. For the
     KPI rail we want the same stickiness but a slightly tighter top
     so the first KPI label sits right under the DashHead bottom rule. */
  top: 12px;
}
/* Sticky behaviour comes back via the base `.d2-side` rule once
   the comparison-mode style tags are removed from the markup. */

/* ── L1 stats strip (top-of-page KPI grid + tile grid) ─────────
   Mobility-style two-bar pattern adopted at L1: a row of large
   KPI cards on top, then a row of compact icon tiles. Cherry-
   picked from `dashboard.css` so yardoverview.html doesn't need to
   load that whole stylesheet (which contains many unrelated
   rules that would conflict with current shell). Tokens swapped to current shell
   equivalents (`--d-bg-card`, `--d-line`, `--d-ink`, etc.). */
/* L1 stats strip — six paired columns. Each column (`.kpi-pair`)
   is a vertical stack: a KPI card on top + a sub-breakdown tile
   under it. The pair is a single conceptual component (they share
   state and data); the visual pairing reinforces that link. */
.ive-stats-bars {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 10px;
}
.kpi-pair {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-inline-size: 0;
}
@media (max-width: 1279px) {
  .ive-stats-bars { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 720px) {
  .ive-stats-bars { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  .ive-stats-bars { grid-template-columns: 1fr; }
}
/* Split hover-zone tile value (used by the Pass Rate KPI tile so
   the male % and female % halves are independently hoverable). The
   inner spans stay inline so the value reads as one number; only
   the cursor + tooltip reveals the split-zone affordance. */
.ive-tile-value-split { display: inline-flex; align-items: baseline; gap: 2px; }
.ive-tile-value-split > .ive-tile-value-zone {
  cursor: default;
  padding: 0 2px;
  border-radius: 4px;
}
.ive-tile-value-split > .ive-tile-value-divider {
  color: var(--d-ink-3);
  opacity: .6;
  pointer-events: none;
}
.ive-kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 10px;
}
.ive-kpi-lg {
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-inline-size: 0;
  position: relative;
  overflow: hidden;
  box-shadow: var(--d-sh-card);
}
.ive-kpi-lg.is-brand {
  /* Driven by the tenant brand color — `--brand-500` is the
     primary token in the OKLCH ramp and follows the active
     tenant's brand h/c/l. Previously hardcoded to `--chart-1`
     (iVE blue), which didn't adapt when the tenant changed. */
  background:
    radial-gradient(120% 120% at 100% 0%, color-mix(in srgb, var(--brand-500) 18%, transparent), transparent 60%),
    var(--d-bg-card);
  border-color: color-mix(in srgb, var(--brand-500) 30%, var(--d-line));
}
.ive-kpi-lg-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.ive-kpi-lg-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--d-ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[dir="rtl"] .ive-kpi-lg-label { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.ive-kpi-lg-value {
  font-size: var(--fsz-display); /* snapped from 26 to display (28) — on-scale */
  font-weight: 700;
  line-height: 1.05;
  color: var(--d-ink);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  /* Shrink to the text's natural width so the hover area matches
     the actual rendered hero number, not the full card width.
     Prevents hovering blank whitespace next to the number from
     triggering the aggregate tooltip. */
  align-self: flex-start;
  inline-size: max-content;
  max-inline-size: 100%;
}
.ive-kpi-lg-sub {
  font-size: var(--fsz-label);
  color: var(--d-ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ive-kpi-lg-foot { margin-block-start: auto; }
/* Spark sits on the foot row alongside the sub-text. Sub takes
   its natural width; the spark grows to fill the remaining row
   width via flex:1 (with a sensible min-width so the trend stays
   legible at narrow card widths). */
.ive-kpi-spark {
  flex: 1 1 auto;
  min-inline-size: 80px;
  block-size: 28px;
  position: relative;
  display: block;
}
.ive-kpi-spark .kpi-spark { inline-size: 100%; block-size: 100%; }

/* `.ive-trend` styling MIGRATED to `.pill.is-{ok|err|neutral}` + `.pill-icon`.
   The class names `ive-trend.is-{up|down}` remain on the markup as
   addressing hooks. Only interaction behavior (help cursor + focus
   ring) stays here — those are trend-pill-specific affordances that
   don't belong in the base pill primitive. */
.ive-trend { outline: none; font-variant-numeric: tabular-nums; }
.ive-trend:focus-visible {
  box-shadow: 0 0 0 2px var(--bg-card, #fff), 0 0 0 4px var(--brand-500);
}

/* TruncTip body — surfaces the full text of a label on hover (used
   when narrow strip widths or Arabic localization clip ellipsis-
   truncated text). Single line, body size, ink-1 color. */
.trunc-tip-text {
  /* Size + weight + color all inherited from .chart-tip so every
     tooltip reads identically. Only layout properties kept here. */
  line-height: 1.3;
  white-space: nowrap;
}

/* Trend-pill hover tooltip body — two stacked lines inside the
   shared ChartTip portal. Direction headline is bold ink; comparison
   window is subdued and smaller for a "primary + supporting" feel. */
/* Two-line trend tip — both lines stay at 12px (label/caption tier).
   System rule: no tooltip text exceeds 12px. The dir line uses LABEL
   typography (12 / 500 / 0.02em) for headline weight; the cmp line
   uses CAPTION typography (12 / 400 / 0) for supporting context. */
.ive-trend-tip-dir {
  font-size: var(--fsz-label);
  font-weight: 500;
  letter-spacing: var(--ls-label);
  color: var(--tooltip-fg);
  line-height: 1.3;
  white-space: nowrap;
}
.ive-trend-tip-cmp {
  margin-block-start: 3px;
  font-size: var(--fsz-caption);
  font-weight: 400;
  letter-spacing: var(--ls-caption);
  color: var(--tooltip-fg-muted);
  line-height: 1.3;
  white-space: nowrap;
}

/* Tile grid — 6 compact icon tiles below the KPI cards.
   Vertical spacing is now handled by the `.ive-stats-bars`
   wrapper (gap: 8px) so this rule only handles internal grid
   layout. */
.ive-tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}
.ive-tile {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  padding: 12px 14px;
  min-inline-size: 0;
  box-shadow: var(--d-sh-card);
}
.ive-tile-icon {
  flex: 0 0 40px;
  inline-size: 40px;
  block-size: 40px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: color-mix(in srgb, var(--chart-1) 18%, transparent);
  color: var(--chart-1);
}
.ive-tile-icon svg { inline-size: 22px; block-size: 22px; }
.ive-tile.is-brand   .ive-tile-icon { background: color-mix(in srgb, var(--chart-1)  18%, transparent); color: var(--chart-1); }
.ive-tile.is-info    .ive-tile-icon { background: color-mix(in srgb, var(--chart-3)  18%, transparent); color: var(--chart-3); }
.ive-tile.is-ok      .ive-tile-icon { background: color-mix(in srgb, var(--d-ok)     18%, transparent); color: var(--d-ok); }
.ive-tile.is-purple  .ive-tile-icon { background: color-mix(in srgb, var(--d-purple) 18%, transparent); color: var(--d-purple); }
.ive-tile.is-warn    .ive-tile-icon { background: color-mix(in srgb, var(--d-warn)   18%, transparent); color: var(--d-warn); }
.ive-tile.is-err     .ive-tile-icon { background: color-mix(in srgb, var(--d-err)    18%, transparent); color: var(--d-err); }
.ive-tile.is-neutral .ive-tile-icon { background: color-mix(in srgb, var(--d-ink)     8%, transparent); color: var(--d-ink-2); }
.ive-tile-body {
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.ive-tile-value {
  font-size: var(--fsz-h2);
  font-weight: 600;
  line-height: 1.1;
  color: var(--d-ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
/* Tile label — typographically aligned with the KPI hero label so
   each paired column reads as one unit (label + value, label + value).
   Same size, weight, tracking, case, and ink as `.ive-kpi-lg-label`. */
.ive-tile-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--d-ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[dir="rtl"] .ive-tile-label { font-size: var(--fsz-label); letter-spacing: 0; text-transform: none; }
.ive-tile-sub { font-size: var(--fsz-caption); color: var(--d-ink-4); margin-block-start: 2px; }

/* Card 4 (Tests Per Hour) chart container inside its DigestCard.
   The DigestCard body uses gap: 12px; this slot just needs to
   reserve a sensible chart height + a tiny bleed so the area
   shape touches the card's inner padding edges. */
.kpi-digest-chart {
  block-size: 96px;
  position: relative;
  margin-block-start: 2px;
  margin-inline: -4px;
}
.kpi-digest-chart .kpi-bigarea-svg { inline-size: 100%; block-size: 100%; }

.d2-digest-head-right {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}
.d2-digest-head-meta {
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
}
.d2-digest-head-meta b { color: var(--d-ink); font-weight: 600; }
/* Full-width sparkline below the hero — gets the entire card
   width since the supplementary fact (e.g. "130 in progress")
   now sits inline with the hero number in the label slot. 40px
   tall reads as "trend signal" without dominating the card. */
.d2-digest-spark-full {
  block-size: 40px;
  position: relative;
  margin-block-start: 4px;
  margin-inline: -4px;
}
.d2-digest-spark-full .kpi-spark { inline-size: 100%; block-size: 100%; }
/* Standalone foot line — used by Card 4 (Tests Per Hour) where
   the body is a chart and the foot carries the peak-time caption. */
.d2-digest-foot-line {
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  padding-block-start: 4px;
  border-block-start: 1px solid var(--d-line-soft);
}
.d2-digest-foot-line b { color: var(--d-ink); font-weight: 600; }

/* `.kpi-delta` styling MIGRATED to `.pill.is-{ok|err|neutral}` + `.pill-icon`.
   Class names + .is-{up|down|flat} kept on markup as addressing hooks;
   tabular-nums stays here since it's KPI-specific. */
.kpi-delta { font-variant-numeric: tabular-nums; }
.kpi-delta-pct { font-weight: 500; opacity: 0.85; }
/* Sub-label next to the delta number — "vs last week" / "vs yesterday".
   Quiet text-tone, picks up the pill's foreground color via inheritance
   but at lower opacity so it reads as supporting info. */
.kpi-delta-sub { font-weight: 500; opacity: 0.75; font-size: var(--fsz-caption); }

/* CSS-driven hover tooltip for elements carrying [data-tip="..."].
   Replaces the native `title` attribute (which had inconsistent
   delay + got intercepted by the institute card's anchor wrapper).
   Renders as a styled bubble above the element with a tiny arrow.
   Tooltip is hidden by default; shows on hover/focus. */
/* Earlier this file had a SECOND tooltip system here that redefined
   [data-tip]::after AND added a [data-tip]::before arrow. That
   ::before arrow was never given the same anchor-flip overrides as
   the bubble (topnav, .auth-controls, [data-tip-align="bottom"]
   anchor the bubble below the element), so the arrow stayed
   stranded ABOVE the button while the bubble dropped below — the
   "weird small box on hover" symptom users saw on the login page
   language/theme buttons and other below-anchored CTAs. The
   duplicate block has been removed; the canonical tooltip system
   lives at the top of this file (line ~46) and uses ::after only,
   so adding a new anchor variant only needs to override the bubble
   position. */
.kpi-live {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--d-err);
  padding: 2px 8px;
  border-radius: var(--r-pill);
  background: var(--d-err-soft);
}
.kpi-live-dot {
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: var(--d-err); color: var(--d-err);
  animation: pill-dot-pulse 1.6s ease-out infinite;
}

.kpi-spark-wrap, .kpi-big-wrap { position: relative; inline-size: 100%; block-size: 100%; }
.kpi-spark, .kpi-big { inline-size: 100%; block-size: 100%; display: block; overflow: visible; }
/* Legacy SVG-text axis labels — retired; the wide chart card now uses
   HTML labels rendered via .kpi-big-axis-row so the text doesn't get
   horizontally stretched by preserveAspectRatio="none" on the SVG.
   Kept the rule (no-op) in case any other surface still uses it. */
.kpi-big-axis {
  font-size: var(--fsz-caption);
  fill: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}
/* HTML overlay: axis tick labels positioned by % to match the SVG
   data points, but rendered in normal HTML so glyph aspect stays
   correct regardless of card width. (May 2026: hidden — the chart
   now reads as a pure trend line; rich label still surfaces via
   the hover tooltip.) */
.kpi-big-axis-row {
  display: none;
}
.kpi-big-axis-tick {
  position: absolute;
  inset-block-end: 0;
  transform: translateX(-50%);
  font-size: var(--fsz-caption);
  font-weight: 500;
  color: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* First/last labels align to their edge to avoid overflowing past
   the card boundary (translate(-50%) on the very-left tick puts half
   the text outside the card; same problem on the right). */
.kpi-big-axis-tick.is-first { transform: translateX(0); }
.kpi-big-axis-tick.is-last  { transform: translateX(-100%); }
[dir="rtl"] .kpi-big-axis-tick { transform: translateX(50%); }
[dir="rtl"] .kpi-big-axis-tick.is-first { transform: translateX(0); }
[dir="rtl"] .kpi-big-axis-tick.is-last  { transform: translateX(100%); }

/* ── INSTITUTE CARD ─────────────────────────────────────────── */
.inst-card {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 16px 18px;
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  text-decoration: none;
  color: var(--d-ink);
  box-shadow: var(--d-sh-card);
  transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
  position: relative;
  /* Grid items default to `min-width: auto` (= shrinks to content's
     intrinsic min-width). On narrow viewports the card's hero row
     (rank + name + delta pill) has a 300+px intrinsic width that
     refuses to shrink, causing the card to overflow its 1fr grid
     track on mobile. `min-width: 0` lets the card shrink with the
     track; descendant rules below allow inner content to wrap or
     truncate cleanly. */
  min-width: 0;
}
.inst-card:hover {
  border-color: color-mix(in oklab, var(--d-indigo) 35%, var(--d-line));
  box-shadow: var(--d-sh-card-hover);
  transform: translateY(-1px);
  /* Lift the hovered card above siblings so its tooltips (rank,
     delta) and pass-rate side popover float cleanly over the next
     card instead of being clipped by the next card's stacking. */
  z-index: 5;
}
.inst-card-head { display: flex; align-items: center; gap: 12px; }
.inst-card-tile {
  /* 48×36 (1.33:1 landscape — close to square so the border-radius
     is tightened to 8px to keep the corner from reading too round
     on the smaller frame). Initials-fallback chip fills the box;
     logo variant uses equal 4px padding (see `--logo` block below)
     and `object-fit: contain` so wordmark lockups read at full
     width while square marks letterbox cleanly. */
  inline-size: 48px;
  block-size: 36px;
  border-radius: 8px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  font-weight: 700;
  font-size: var(--fsz-body);
  letter-spacing: 0.04em;
  color: white;
  background: var(--d-indigo);
}
.inst-card-tile[data-tone="A"] { background: oklch(0.55 0.20 290); }   /* purple */
.inst-card-tile[data-tone="B"] { background: oklch(0.60 0.16 50);  }   /* orange */
.inst-card-tile[data-tone="C"] { background: oklch(0.62 0.15 220); }   /* blue */
.inst-card-tile[data-tone="D"] { background: oklch(0.62 0.18 158); }   /* green */
.inst-card-tile[data-tone="E"] { background: oklch(0.55 0.22 350); }   /* magenta */
.inst-card-tile[data-tone="F"] { background: oklch(0.62 0.16 195); }   /* teal */
.inst-card-tile[data-tone="G"] { background: oklch(0.58 0.19 18);  }   /* red */
.inst-card-tile[data-tone="H"] { background: oklch(0.65 0.18 95);  }   /* yellow-green */
/* When the institute has a real logo SVG, the tile becomes a clean
   square frame: transparent fill, hairline border, logo image
   centered inside. The frame keeps consistent visual mass across the
   11 cards regardless of logo aspect ratio. The double-class
   selector overrides the .inst-card-tile[data-tone="X"] colored
   backgrounds above without needing !important. */
.inst-card-tile.inst-card-tile--logo {
  background: transparent;
  border: 1px solid var(--d-line);
  /* Equal 4px padding on all sides → 40×28 inner canvas. Equal
     T/B/L/R matches the inset users expect on a square-ish frame. */
  padding: 4px;
}
.inst-card-tile--logo > img {
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
  object-fit: contain;
  display: block;
}
[data-theme="dark"] .inst-card-tile.inst-card-tile--logo {
  /* Same treatment as the list-view tile — near-white surface for
     the logo to sit on, soft line border, no glow. */
  background: oklch(0.94 0 0);
  border: 1px solid color-mix(in oklab, var(--d-line) 50%, transparent);
}
.inst-card-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.inst-card-name {
  font-size: var(--fsz-h3); font-weight: 700;
  color: var(--d-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  letter-spacing: -0.01em;
}
.inst-card-loc {
  font-size: var(--fsz-label);
  color: var(--d-ink-3);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}
.inst-card-loc-sep { opacity: 0.7; }
/* Branches chip — small folder icon + count + label, all inline. */
.inst-card-loc-branches {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.inst-card-loc-branches > svg {
  flex-shrink: 0;
  color: var(--d-ink-3);
}
.inst-card-rank {
  align-self: flex-start;
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--d-ink-4);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.inst-card-rank.is-top { color: var(--d-ok); }

/* Hero — Tests count + delta. Replaces the old PASS RATE % big number;
   pass-rate now lives inside the bar caption + hover popover. */
/* Live-update flip — flip-board / ticker effect when AnimatedNum's
   value changes. The wrapper becomes a clipping mask; old value
   slides up and out, new value slides up from below. Plus a brief
   underline-halo in the brand-ok color so the change registers in
   peripheral vision. ~520ms total animation. */
.tnum.is-flip {
  position: relative;
  display: inline-block;
  overflow: hidden;
  vertical-align: baseline;
  /* The halo underline. Animates from brand-ok → transparent. */
  --num-flip-halo: var(--d-ok, oklch(0.62 0.12 175));
}
.tnum.is-flip::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 1px;
  block-size: 2px;
  background: var(--num-flip-halo);
  border-radius: 1px;
  animation: num-flip-halo 520ms ease forwards;
  pointer-events: none;
}
.tnum.is-flip .num-flip-old,
.tnum.is-flip .num-flip-new {
  display: block;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
}
.tnum.is-flip .num-flip-old {
  animation: num-flip-out 520ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.tnum.is-flip .num-flip-new {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  animation: num-flip-in 520ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes num-flip-out {
  0%   { transform: translateY(0);     opacity: 1; }
  100% { transform: translateY(-100%); opacity: 0; }
}
@keyframes num-flip-in {
  0%   { transform: translateY(100%);  opacity: 0; }
  60%  {                                opacity: 1; }
  100% { transform: translateY(0);     opacity: 1; }
}
@keyframes num-flip-halo {
  0%   { opacity: 0;   transform: scaleX(0.4); }
  20%  { opacity: 0.9; transform: scaleX(1);   }
  100% { opacity: 0;   transform: scaleX(1);   }
}

/* `flex-wrap: wrap` lets the delta pill drop to a new line on narrow
   viewports (mobile) instead of forcing the card to grow past its
   1fr grid track. min-width: 0 lets the number text shrink/truncate
   if needed. */
.inst-card-hero { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
.inst-card-hero-num {
  /* Sits below the rail KPI number (26px) so the institute grid
     reads as content and the rail KPIs read as summary chrome.
     Type ladder: KPI rail num 26 → inst hero 24 → inst stat 18. */
  font-size: var(--fsz-h1);
  font-weight: 700;
  color: var(--d-ink);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
}
.inst-card-hero-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--d-ink-3);
}
.inst-card-hero-spacer { flex: 1; }

/* Pass-rate bar — single muted fill, calmer than the old 3-segment
   vivid version. Hover surfaces the full breakdown via popover. */
.inst-card-rate {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding-block: 2px 8px;
  cursor: default;
  outline: none;
}
/* Divider centered in the gap between rate and stats — sits at the
   midpoint via a pseudo-element so it has equal breathing room on
   both sides. Slightly stronger color than line-soft so it reads
   clearly without dominating. */
.inst-card-rate::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: -7px;
  block-size: 1px;
  background: var(--d-line);
}
.inst-card-rate-bar {
  block-size: 6px;
  background: transparent;
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 2px;
  /* At-rest the whole bar is dimmed to 50% so it reads as
     quiet supporting context; on card hover it pops to full
     opacity to surface the breakdown. Pairs with the per-segment
     color shifts (pass/fail/absent/pending) below for a strong
     calm-to-emphasized transition. */
  opacity: 0.5;
  transition: opacity 180ms ease;
}
.inst-card:hover .inst-card-rate-bar,
.inst-card:focus-within .inst-card-rate-bar {
  opacity: 1;
}
/* Each category is a fully rounded pill at full bar height. Widths
   are proportional to share of total. Pass uses its bold foreground
   token always (headline). The other three sit on the lighter -bg
   tokens at-rest and pop to the bold foreground token on hover —
   gives a calm reading at-rest with full color reveal on intent. */
.inst-card-rate-seg {
  display: block;
  block-size: 6px;
  border-radius: var(--r-pill);
  transition: inline-size 280ms ease, background 180ms ease;
}
.inst-card-rate-seg.is-pass    { background: var(--pass); }
.inst-card-rate-seg.is-fail    { background: var(--fail-border); }
.inst-card-rate-seg.is-absent  { background: var(--absent-border); }
.inst-card-rate-seg.is-pending { background: var(--pending-border); }
.inst-card:hover .inst-card-rate-seg.is-fail,
.inst-card:focus-within .inst-card-rate-seg.is-fail    { background: var(--fail); }
.inst-card:hover .inst-card-rate-seg.is-absent,
.inst-card:focus-within .inst-card-rate-seg.is-absent  { background: var(--absent); }
.inst-card:hover .inst-card-rate-seg.is-pending,
.inst-card:focus-within .inst-card-rate-seg.is-pending { background: var(--pending); }

/* Earlier "stair-step strip" / spacer rules retired. The bar now
   uses a single-row of 4 segments with fixed per-category heights
   (Pass 4 / Fail 3 / Absent 2 / Pending 2) — see
   .inst-card-rate-seg above. */
.inst-card-rate-cap {
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
  display: flex;
  align-items: baseline;
  gap: 4px;
  line-height: 1;
}
.inst-card-rate-cap > .tnum {
  color: var(--d-ink);
  font-weight: 600;
}
.inst-card-rate-cap-label {
  font-size: var(--fsz-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--d-ink-3);
}

/* Hover popover — anchored to the FAR RIGHT of the bar, popping up
   above. Stays at the right edge throughout the animation (no
   horizontal transform, only opacity + small Y slide-in). */
/* InstituteRateBar breakdown — INNER content only.
   Outer popover chrome (bg, border, shadow, position, arrow) now comes
   from the wrapping <ChartTip> portal. This class controls only the
   row-stack layout inside that portal. */
.inst-card-rate-pop {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-inline-size: 200px;
}
@keyframes inst-rate-pop-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes inst-rate-pop-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: none; }
}
.inst-card-rate-pop-row {
  display: grid;
  grid-template-columns: 8px 1fr auto auto;
  align-items: center;
  gap: 8px;
  font-variant-numeric: tabular-nums;
}
.inst-card-rate-pop-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
}
.inst-card-rate-pop-row.is-pass   .inst-card-rate-pop-dot { background: var(--pass); }
.inst-card-rate-pop-row.is-fail   .inst-card-rate-pop-dot { background: var(--fail); }
.inst-card-rate-pop-row.is-absent .inst-card-rate-pop-dot { background: var(--absent); }
.inst-card-rate-pop-row.is-pending .inst-card-rate-pop-dot {
  background: var(--pending);
}
.inst-card-rate-pop-label { color: var(--tooltip-fg-muted); font-size: var(--fsz-caption); }
.inst-card-rate-pop-num   { color: var(--tooltip-fg); font-weight: 600; font-size: var(--fsz-label); }
.inst-card-rate-pop-pct   { color: var(--tooltip-fg-muted); font-size: var(--fsz-caption); min-inline-size: 42px; text-align: end; }

/* ── Table rate-bar cell (RateBarCell) — inline variant of the
   InstituteRateBar used inside .branch-cmp-tbl tbody. Same 4-segment
   bar, same hover popover, but laid out horizontally with the
   percentage to the right of the bar. Heights and segment colours
   match the card so a user reading both views sees the same visual
   language. */
.rate-cell {
  /* Number stacks ABOVE the bar (top-left) instead of beside it.
     Matches the overrides cell layout — frees a chunk of column
     width since the number no longer eats horizontal space. */
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  inline-size: 100%;
  position: relative;
}
.rate-cell-bar {
  block-size: 6px;
  background: transparent;
  display: flex;
  align-items: stretch;
  gap: 2px;
  inline-size: 100%;
  min-inline-size: 0;
}
.rate-cell-seg {
  display: block;
  block-size: 6px;
  border-radius: var(--r-pill);
  transition: inline-size 280ms ease, background 180ms ease;
}
.rate-cell-seg.is-pass    { background: var(--pass); }
.rate-cell-seg.is-fail    { background: var(--fail-border); }
.rate-cell-seg.is-absent  { background: var(--absent-border); }
.rate-cell-seg.is-pending { background: var(--pending-border); }
/* Reveal full-saturation colours when the cell itself is hovered,
   the row is hovered (table context), or the cell is keyboard-
   focused. The .rate-cell:hover variant handles standalone uses
   like the maneuver column where there's no table row wrapper. */
.rate-cell:hover .rate-cell-seg.is-fail,
.branch-cmp-tbl tbody tr:hover .rate-cell-seg.is-fail,
.rate-cell:focus-within .rate-cell-seg.is-fail    { background: var(--fail); }
.rate-cell:hover .rate-cell-seg.is-absent,
.branch-cmp-tbl tbody tr:hover .rate-cell-seg.is-absent,
.rate-cell:focus-within .rate-cell-seg.is-absent  { background: var(--absent); }
.rate-cell:hover .rate-cell-seg.is-pending,
.branch-cmp-tbl tbody tr:hover .rate-cell-seg.is-pending,
.rate-cell:focus-within .rate-cell-seg.is-pending { background: var(--pending); }
.rate-cell-num {
  /* Top-left over the bar (matches the overrides cell layout).
     The flex-column parent stacks number above bar; align-self
     keeps it pinned to the start (left edge in LTR, right in
     RTL via logical property).
     Per the project's table-typography rule: only the first
     (identity) column is bold. Numeric cells — including the
     percentage above the bar — match plain `.tnum` table cells:
     13px, regular weight, full ink. The bar carries the visual,
     the number carries the value, and the row identity remains
     the only bolded thing on the row. */
  align-self: flex-start;
  font-size: var(--fsz-body);
  font-weight: 400;
  color: var(--d-ink, var(--ink));
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1;
}
/* Hover popover — anchored to the right edge of the cell, rising
   above the row. Mirror the card's popover styling. */
/* RateBarCell / FirstTimePrCell breakdown — INNER content only.
   Outer popover chrome (bg, border, shadow, position, arrow) now comes
   from the wrapping <ChartTip> portal. This class controls only the
   row-stack layout inside that portal. */
.rate-cell-pop {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-inline-size: 200px;
}
.rate-cell-pop-row {
  display: grid;
  grid-template-columns: 8px 1fr auto auto;
  align-items: center;
  gap: 8px;
  font-variant-numeric: tabular-nums;
}
/* Inline variant — popover used INSIDE another positioned tip
   container (e.g. .bx-chart-col-tip). Strips the absolute
   positioning + box chrome so the parent tip provides those;
   rate-cell-pop just contributes the row layout. */
.rate-cell-pop.is-inline {
  position: static;
  inset: auto;
  box-shadow: none;
  border: 0;
  background: transparent;
  padding: 0;
  /* Fill the wrapping tip card so the row grid can stretch its
     1fr label column across the whole card width — no dead space
     on the trailing edge of the bx-chart-col-tip. */
  min-inline-size: 0;
  inline-size: 100%;
  animation: none;
}
/* Floating variant — popover renders with position:fixed via JS-
   computed coords so it escapes the table-wrap's overflow:auto
   context (otherwise the first body row's popover gets clipped
   by the sticky thead / wrap clipping rect). The inline style
   sets position/left/bottom; this rule clears the base rule's
   logical-inset anchors and bumps z-index above the thead. */
.rate-cell-pop.is-floating {
  inset-block-end: auto;
  inset-inline-end: auto;
  inset-block-start: auto;
  inset-inline-start: auto;
  z-index: var(--z-tooltip);
}
/* Title row for popover variants that need a label (e.g. the
   examiner pass-rate-distribution histogram). */
.rate-cell-pop-title {
  font-weight: 600;
  font-size: var(--fsz-caption);
  color: var(--tooltip-fg-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding-block-end: 4px;
  border-block-end: 1px solid var(--tooltip-border);
  margin-block-end: 2px;
}
/* Histogram popover — no leading dot, two columns: range, count, share. */
.rate-cell-pop.is-histo .rate-cell-pop-row {
  grid-template-columns: 1fr auto auto;
  padding: 2px 4px;
  border-radius: 4px;
}
.rate-cell-pop.is-histo .rate-cell-pop-row.is-active {
  /* Active row on the dark tooltip surface — use a brand-tinted dark
     overlay so the highlight reads against the inverted bg without
     blowing out. */
  background: color-mix(in oklab, var(--brand-500) 22%, transparent);
  color: var(--tooltip-fg);
  font-weight: 600;
}
/* Variant of .rate-cell that hides its trailing % number (used in
   the maneuver column where the % is rendered separately above). */
.rate-cell.is-bar-only .rate-cell-num { display: none; }
.rate-cell.is-bar-only .rate-cell-bar { inline-size: 100%; }
.rate-cell.is-bar-only { inline-size: 100%; }
/* Examiner-table Pass Rate cell — pin to 110px so the bar matches
   the Overrides bar (.exam-ovr-wrap also 110px) and the Top-10
   Fail-Rate bar. Scoped to .d2-section so the Branch Comparison
   table (which lives in section.branch-cmp) keeps its wider bar
   that fills the available column space. */
.d2-section .cell-pr .rate-cell { inline-size: 110px; }
/* Histogram cell wrap — host for the hover popover (the original
   .d2-exam-tbl-histo span doesn't carry the position context). */
.d2-exam-tbl-histo-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.d2-exam-tbl-histo-wrap:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: 4px;
}
.rate-cell-pop-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
}
.rate-cell-pop-row.is-pass        .rate-cell-pop-dot { background: var(--pass); }
.rate-cell-pop-row.is-fail        .rate-cell-pop-dot { background: var(--fail); }
.rate-cell-pop-row.is-absent      .rate-cell-pop-dot { background: var(--absent); }
.rate-cell-pop-row.is-pending     .rate-cell-pop-dot { background: var(--pending); }
/* Faded-green tone — used by FailRateCell's "Other top-10" row to
   match the bar's faded second segment. Same green as `.is-pass`
   but at the lower-saturation `--pass-border` token. */
.rate-cell-pop-row.is-pass-faded  .rate-cell-pop-dot { background: var(--pass-border); }
/* Gender variants — used by the 1st Time Pass Rate cell. The cell
   itself uses an SVG donut (`.ft-donut`), but the popover rows still
   pick up the M/F dot colors via these tokens. */
.rate-cell-pop-row.is-male   .rate-cell-pop-dot { background: var(--d-male); }
.rate-cell-pop-row.is-female .rate-cell-pop-dot { background: var(--d-female); }
/* Donut container — sits in the cell where `.rate-cell-bar` sits in
   the Pass Rate cell. Same height/alignment as a single-row bar so
   the two columns line up vertically. */
.ft-cell .ft-donut {
  flex-shrink: 0;
  block-size: 22px;
  inline-size: 22px;
}
/* Layout override — the FT cell pairs a donut with the % number
   side-by-side on a single row (rather than stacked like the Pass
   Rate cell where the % sits above its bar). Override the inherited
   `.rate-cell { flex-direction: column }` for this variant. */
.rate-cell.ft-cell {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.rate-cell.ft-cell .rate-cell-num { align-self: center; }
.rate-cell-pop-label { color: var(--tooltip-fg-muted); font-size: var(--fsz-caption); }
.rate-cell-pop-num   { color: var(--tooltip-fg); font-weight: 600; font-size: var(--fsz-label); }
.rate-cell-pop-pct   { color: var(--tooltip-fg-muted); font-size: var(--fsz-caption); min-inline-size: 42px; text-align: end; }

/* 2- or 3-col stats row. The 3rd column is conditional (range=today)
   so we use auto-flow with auto-cols to handle either case cleanly. */
.inst-card-stats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 10px;
  padding-block-start: 4px;
}
.inst-card-stat {
  display: flex;
  flex-direction: column;
  gap: 3px;
  /* Logical inline-start alignment so the label + number stack to
     the LEFT in LTR and the RIGHT in RTL (Arabic reading direction).
     Without this the inline-flex label and the number text would
     each sit on the inline-start of the column independently, which
     is correct, but `text-align: start` also forces a consistent
     baseline edge for the bare number text. */
  text-align: start;
  align-items: flex-start;
  /* Grid items default min-width: auto. On mobile the "CONTRIBUTION"
     label is wider than a 1fr column third, forcing the parent grid
     to overflow. min-width: 0 + text-overflow lets the label
     truncate cleanly inside its column. */
  min-width: 0;
}
.inst-card-stat-label {
  /* Existing rules below set font; this allows truncation when the
     stat column shrinks below the intrinsic label width on mobile. */
  max-inline-size: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.inst-card-stat-label {
  /* Caption-sized to match `.inst-card-hero-label` and
     `.inst-card-loc` — was 9.5px which read as a footnote next to
     the rest of the card text. */
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--d-ink-3);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-ui);
}
/* Arabic glyphs don't have casing (so `text-transform: uppercase`
   is a no-op) and don't benefit from the LTR tracking — kill the
   spacing in RTL so labels stay tight. */
[dir="rtl"] .inst-card-stat-label { letter-spacing: 0; }
/* Small info-mark next to a stat label. Hidden at-rest so the
   stat row reads as a clean label/value pair; revealed only when
   the card is hovered or focused, signalling that explainer copy
   is available. Tints brand on its own hover. */
.inst-card-stat-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 13px;
  block-size: 13px;
  color: var(--d-ink-4);
  opacity: 0;
  transition: opacity 160ms ease, color var(--t-fast);
}
.inst-card:hover .inst-card-stat-info,
.inst-card:focus-within .inst-card-stat-info,
.inst-card-stat-info:hover,
.inst-card-stat-info:focus-visible { opacity: 1; }
.inst-card-stat-info:hover { color: var(--brand-600); }
.inst-card-stat-num {
  font-size: var(--fsz-h3);
  font-weight: 700;
  /* Mid gray — clear visual step-down from the bold hero so the
     eye lands on the hero first and stats read as supporting data. */
  color: var(--d-ink-3);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.inst-card-stat-unit { font-size: var(--fsz-caption); font-weight: 500; color: var(--d-ink-3); margin-inline-start: 2px; }

/* Override inst-grid layout — switch to 3-col grid that matches the reference */
.app:has(.inst-card) .inst-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}
@media (max-width: 1100px) { .app:has(.inst-card) .inst-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px)  { .app:has(.inst-card) .inst-grid { grid-template-columns: 1fr; } }

/* ── IveInstitutesMap (revamped) ───────────────────────────────
   Real Mapbox map of Dubai with logo-tile markers per institute
   (always-on name label, zoom-aware short → full), a side panel,
   and a unified hover/pin card matching the InstituteCard look.
   Drill mode (panel-click) fades non-selected institutes and
   drops branch markers; auto-back-out on significant zoom-out. */
.ive-map-wrap {
  position: relative;
  inline-size: 100%;
  block-size: clamp(560px, 72vh, 760px);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--bg-sunken);
  /* Side panel sits absolute over the map's left edge — keeps the
     map full-bleed while the list overlays on top. */
}
/* Map container fills its wrap. The combined-class selector on the
   second rule has specificity 0,2,0, beating Mapbox's own
   `.mapboxgl-map { position: relative }` rule (loaded after page.css
   from the CDN) which would otherwise collapse the container to 0
   height once Mapbox initializes. */
.ive-map { position: absolute; inset: 0; block-size: 100%; }
.ive-map.mapboxgl-map { position: absolute; inset: 0; block-size: 100%; }
.ive-map-loading {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: var(--ink-3); font-size: var(--fsz-body);
  background: var(--bg-sunken);
  pointer-events: none;
}
/* Branch marker pill — used by L1 when drilling into an institute.
   Single colour bullet + label, chip-style legibility. */
.ive-branch-marker {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 4px;
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 4px 12px -4px rgba(15, 17, 21, 0.16);
  font-size: var(--fsz-caption);
  color: var(--d-ink);
  white-space: nowrap;
}
.ive-branch-marker:hover {
  border-color: color-mix(in oklab, var(--d-indigo) 35%, var(--d-line));
  box-shadow: 0 6px 16px -4px rgba(15, 17, 21, 0.22);
}
.ive-branch-marker-pin {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 50%;
  background: var(--d-indigo);
  flex-shrink: 0;
}
.ive-branch-marker.is-hq .ive-branch-marker-pin { background: var(--d-purple); }
.ive-branch-marker.is-hq { border-color: color-mix(in oklab, var(--d-purple) 35%, var(--d-line)); }
.ive-branch-marker-label { font-weight: 600; }

/* Lock veil — invisible-but-interactive overlay covering the map
   wrap while interaction is disabled. The map stays fully visible
   (no dark wash, no blur) — only the centered "click or drag"
   pill signals the locked state. The veil intercepts mouse events
   so markers/tooltips stay quiet until the user re-engages. */
.ive-map-veil {
  position: absolute;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: transparent;
  cursor: pointer;
  animation: ive-map-veil-in 200ms ease-out;
}
.ive-map-veil:focus-visible { outline: 2px solid var(--d-indigo); outline-offset: -4px; }
@keyframes ive-map-veil-in { from { opacity: 0; } to { opacity: 1; } }

/* "Click or drag to interact" hint — centered pill. Larger type
   and a stronger shadow so it stays legible without the veil's
   background tint to anchor it. The leading hand glyph runs a
   subtle looped tap animation (scale + slight rotate) to draw
   the eye toward the call to action. */
.ive-map-engage {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 11px 20px;
  background: rgba(15, 17, 21, 0.86);
  color: white;
  border-radius: var(--r-pill);
  font-size: var(--fsz-body);
  font-weight: 600;
  letter-spacing: 0.01em;
  pointer-events: none;
  box-shadow:
    0 12px 32px -10px rgba(15, 17, 21, 0.45),
    0 2px 6px -2px rgba(15, 17, 21, 0.25);
  animation: ive-map-engage-in 240ms ease-out;
}
.ive-map-engage > svg {
  color: oklch(0.86 0.12 60);
  flex-shrink: 0;
  display: block;
  inline-size: 18px;
  block-size: 18px;
  animation: ive-map-engage-tap 1.8s cubic-bezier(0.34, 1.4, 0.5, 1) infinite;
  transform-origin: 12px 16px;
}
@keyframes ive-map-engage-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes ive-map-engage-tap {
  0%, 60%, 100% { transform: scale(1) rotate(0deg); }
  20%           { transform: scale(1.15) rotate(-6deg); }
  40%           { transform: scale(1.05) rotate(6deg); }
}
/* ── Mapbox controls polish ──
   Match the mobility fleet map's quiet treatment: controls are
   semi-transparent and lift to full opacity on hover (or on hover of
   the wrap, so users see them when they engage the map).
   LTR: controls on physical RIGHT, panel on physical LEFT (Mapbox
   default). RTL: flipped — controls on physical LEFT, panel on
   physical RIGHT — matches the natural inline-start anchor used by
   the rest of the dashboard chrome. */
/* Attribution + Mapbox logo are kept in the DOM (Mapbox ToS) but
   visually hidden and not focusable per design call. */
.ive-map-wrap .mapboxgl-ctrl-bottom-right,
.ive-map-wrap .mapboxgl-ctrl-bottom-left { opacity: 0; pointer-events: none; }
.ive-map-wrap .mapboxgl-ctrl-top-right {
  margin-block-start: 12px;
  margin-right: 12px;
  margin-left: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* RTL: flip the Mapbox `top-right` container to physical left so the
   controls sit on the opposite edge from the AR-flipped panel. */
[dir="rtl"] .ive-map-wrap .mapboxgl-ctrl-top-right {
  right: auto;
  left: 0;
  margin-left: 12px;
  margin-right: 0;
}
/* Mapbox itself adds margin to each .mapboxgl-ctrl-group child of
   the column. Override so our flex `gap` is the only spacing
   that decides distance between groups (tighter, predictable). */
.ive-map-wrap .mapboxgl-ctrl-top-right > .mapboxgl-ctrl { margin: 0 !important; }
/* Nav + fullscreen controls share one column with consistent
   spacing; opacity recedes when no interaction is happening. */
.ive-map-wrap .mapboxgl-ctrl-group {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: 0 4px 14px -6px rgba(15, 17, 21, 0.18), 0 1px 0 0 rgba(15, 17, 21, 0.04);
  /* overflow: visible (not hidden) so child buttons' [data-tip]
     ::after tooltips can escape the rounded-corner mask. Corner
     rounding handled by the first/last button border-radius rules
     below instead. */
  overflow: visible;
  opacity: 0.55;
  transition: opacity var(--t-fast);
}
/* Lift the group to fully opaque on hover so the child [data-tip]
   ::after tooltip — whose own opacity transitions 0→1 on hover —
   isn't multiplied down to 0.55 by the parent. */
.ive-map-wrap .mapboxgl-ctrl-group:hover { opacity: 1; }
/* Corner rounding moves from the group's `overflow: hidden` mask to
   per-button radii so tooltips can escape. */
.ive-map-wrap .mapboxgl-ctrl-group button:first-child {
  border-start-start-radius: var(--r-md);
  border-start-end-radius: var(--r-md);
}
.ive-map-wrap .mapboxgl-ctrl-group button:last-child {
  border-end-start-radius: var(--r-md);
  border-end-end-radius: var(--r-md);
}
.ive-map-wrap .mapboxgl-ctrl-group button:only-child {
  border-radius: var(--r-md);
}
/* Keyboard focus keeps controls visible regardless of activity-fade
   state — accessibility. Hover/cursor-activity is driven by the
   `.is-active` rule below (lines ~12754) which uses the JS 5s
   mousemove timer; we don't add a hover rule here because it
   would override the activity timer's idle fade. */
.ive-map-wrap.is-interactive .mapboxgl-ctrl-group:focus-within { opacity: 1; }
.ive-map-wrap .mapboxgl-ctrl-group button {
  inline-size: 32px;
  block-size: 32px;
  background-color: transparent;
  border: 0;
  color: var(--ink-2, var(--ink));
  transition: background-color var(--t-fast);
}
.ive-map-wrap .mapboxgl-ctrl-group button:hover {
  background-color: var(--brand-tint, color-mix(in oklab, var(--brand-600) 10%, transparent));
}
.ive-map-wrap .mapboxgl-ctrl-group button + button {
  border-block-start: 1px solid var(--line);
}
.ive-map-wrap .mapboxgl-ctrl-icon { filter: none; }
[data-theme="dark"] .ive-map-wrap .mapboxgl-ctrl-icon { filter: invert(0.92); }
/* Override Mapbox's built-in fullscreen icon with a recognisable
   4-corner-arrows glyph. Specificity needs to beat Mapbox's own
   `.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon`
   rule (0,3,1) — adding `button` element + the `.mapboxgl-ctrl`
   wrapper class gives us 0,4,1 which wins. */
.ive-map-wrap .mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon {
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5'/%3E%3C/svg%3E");
  background-size: 18px 18px;
  background-position: center;
  background-repeat: no-repeat;
}
.ive-map-wrap .mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon {
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5'/%3E%3C/svg%3E");
  background-size: 18px 18px;
  background-position: center;
  background-repeat: no-repeat;
}

/* ── Side panel (institute list) ── */
/* LTR: panel on physical LEFT (original layout). RTL: panel on
   physical RIGHT — uses logical `inset-inline-start` so the flip is
   automatic with the page direction. */
.ive-map-panel {
  position: absolute;
  inset-block-start: 12px;
  inset-inline-start: 12px;
  inline-size: 296px;
  max-block-size: calc(100% - 24px);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: 0 6px 22px -10px rgba(15, 17, 21, 0.18), 0 1px 0 0 rgba(15, 17, 21, 0.04);
  z-index: 5;
  display: flex;
  flex-direction: column;
  /* `overflow: visible` so [data-tip] tooltips on the toggle
     button and the row rank pills can escape the panel — they
     were getting clipped by the panel's rounded-corner mask.
     Rounded corners on the header + list are handled below by
     applying matching border-radius to those direct children. */
  overflow: visible;
  opacity: 0.55;
  transition: opacity var(--t-fast);
}
/* Round the corners on the header and list to match the panel's
   border-radius — replaces what `overflow: hidden` used to do. */
.ive-map-panel-head {
  border-start-start-radius: var(--r-lg);
  border-start-end-radius:   var(--r-lg);
}
.ive-map-panel-list {
  border-end-start-radius: var(--r-lg);
  border-end-end-radius:   var(--r-lg);
}
/* Keyboard focus keeps panel visible regardless of activity-fade
   state. Cursor-activity is driven by the `.is-active` rule below
   (lines ~12754) which uses the same JS 5s mousemove timer that
   already drives the controls. */
.ive-map-wrap.is-interactive .ive-map-panel:focus-within { opacity: 1; }
.ive-map-panel-head {
  display: flex;
  align-items: center;
  /* Title and count cluster together (left side), toggle pinned to
     the opposite edge via the `margin-inline-start: auto` rule on
     `.ive-map-panel-toggle` below. */
  gap: 8px;
  padding: 10px 12px;
  border-block-end: 1px solid var(--line);
  background: var(--bg-sunken);
}
.ive-map-panel-head .ive-map-panel-toggle { margin-inline-start: auto; }
.ive-map-panel-toggle {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  inline-size: 26px;
  block-size: 26px;
  /* Center the chevron with flex (was grid place-items but the
     SVG needs explicit flex centering for stable position when the
     border + background flip on hover). */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  color: var(--ink-3);
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
  flex-shrink: 0;
}
.ive-map-panel-toggle > svg { display: block; }
.ive-map-panel-toggle:hover,
.ive-map-panel-toggle:focus-visible {
  color: var(--ink);
  background: color-mix(in oklab, var(--brand-600) 8%, transparent);
  border-color: var(--line);
  outline: none;
}
[dir="rtl"] .ive-map-panel-toggle svg { transform: scaleX(-1); }
/* Collapsed-state pill — replaces the full panel when closed.
   Same anchor as the panel itself: physical-left in LTR, physical-
   right in RTL, via the logical `inset-inline-start`. (Previously
   the pill carried `direction: ltr` to keep the chevron-glyph on
   its physical-left edge, but that also re-resolved
   `inset-inline-start` to physical-LEFT in RTL, sticking the pill
   to the wrong corner of the map. We let parent direction drive
   both positioning AND flex order now — chevron flips with `dir`
   naturally, and the SVG path swap in [dir="rtl"] keeps the arrow
   pointing the correct way.) */
.ive-map-panel-open {
  position: absolute;
  inset-block-start: 12px;
  inset-inline-start: 12px;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 8px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-2, var(--ink));
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
  box-shadow: 0 4px 14px -6px rgba(15, 17, 21, 0.18), 0 1px 0 0 rgba(15, 17, 21, 0.04);
  /* Match the fade behaviour of `.mapboxgl-ctrl-group` (0.55 idle,
     1.0 hover) so the CTA sits at the same visual weight as the
     map controls and the other in-fullscreen pills. */
  opacity: 0.55;
  transition: opacity var(--t-fast), background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.ive-map-panel-open:hover,
.ive-map-panel-open:focus-visible {
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand-600) 40%, var(--line));
  transform: translateX(var(--ive-pill-hover-x, 2px));
  /* Bump above the selection chip's stacking context so the
     tooltip (rendered as `::after` below the button via
     `data-tip-align="bottom"`) doesn't get clipped by the chip
     that sits at top:50px directly underneath. */
  z-index: 10;
}
[dir="rtl"] .ive-map-panel-open svg { transform: scaleX(-1); }
[dir="rtl"] .ive-map-panel-open { --ive-pill-hover-x: -2px; }

/* Selection chip — sits below the open-panel pill when the panel
   is collapsed AND an institute is currently drilled. Gives the
   user a "you have an active selection" indicator without having
   to expand the panel. Click clears the selection. */
.ive-map-selection-chip {
  position: absolute;
  /* Pill above is 30px tall (12 + height + 8 spacing); chip starts at 50px. */
  inset-block-start: 50px;
  inset-inline-start: 12px;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px 5px 8px;
  background: var(--bg-raised);
  border: 1px solid color-mix(in oklab, var(--brand-600) 40%, var(--line));
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-2, var(--ink));
  cursor: pointer;
  box-shadow: 0 4px 14px -6px rgba(15, 17, 21, 0.18), 0 1px 0 0 rgba(15, 17, 21, 0.04);
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
  animation: ive-map-pop-in 220ms ease-out;
}
.ive-map-selection-chip:hover {
  background: color-mix(in oklab, var(--brand-600) 12%, var(--bg-raised));
  border-color: color-mix(in oklab, var(--brand-600) 60%, var(--line));
  color: var(--brand-700);
}
.ive-map-selection-chip-dot {
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: var(--brand-600);
  flex-shrink: 0;
}
.ive-map-selection-chip-label {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  font-size: var(--fsz-caption);
}
.ive-map-selection-chip-name {
  color: var(--brand-700);
  font-weight: 700;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  max-inline-size: 220px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ive-map-selection-chip > svg {
  color: var(--ink-3);
  margin-inline-start: 2px;
}
.ive-map-selection-chip:hover > svg { color: var(--brand-700); }
@keyframes ive-map-pop-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.ive-map-wrap.is-panel-closed .ive-map-panel {
  /* Slide the panel out to the LTR-physical-left when collapsed.
     RTL inverts via the override below so it always slides off the
     panel's anchor edge. */
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
  transition: transform var(--t-med), opacity var(--t-med);
}
.ive-map-wrap.is-panel-open .ive-map-panel {
  transform: translateX(0);
  /* Opacity intentionally NOT set here — falls through to the base
     `.ive-map-panel { opacity: 0.55 }` rule so the panel matches
     the map controls' idle-fade behaviour. The hover/focus rules
     above take it to 1 when the user engages with the map. */
  transition: transform var(--t-med), opacity var(--t-fast);
}
[dir="rtl"] .ive-map-wrap.is-panel-closed .ive-map-panel {
  transform: translateX(calc(100% + 24px));
}
.ive-map-panel-title {
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
.ive-map-panel-count {
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-3);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  padding: 2px 7px;
  border-radius: var(--r-pill);
}
.ive-map-panel-list {
  list-style: none;
  margin: 0;
  padding: 4px 0;
  overflow-y: auto;
  flex: 1;
}
.ive-map-panel-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  transition: background var(--t-fast);
}
.ive-map-panel-row:hover { background: color-mix(in oklab, var(--brand-600) 6%, transparent); }
.ive-map-panel-row { position: relative; }
.ive-map-panel-row.is-selected {
  background: color-mix(in oklab, var(--brand-600) 12%, transparent);
}
.ive-map-panel-row.is-selected::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0;
  inset-block-end: 0;
  inline-size: 3px;
  background: var(--brand-600);
}

/* Rank label — natural typography, no background pill (matches the
   InstituteCard rank treatment). The tier color lives only in the
   text, not in a fill. Top-3 keep the brand color so they pop;
   others use ink-3. */
.ive-map-panel-rank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 24px;
  block-size: 22px;
  background: transparent;
  font-size: var(--fsz-caption);
  font-weight: 700;
  color: var(--ink-3);
  letter-spacing: 0.02em;
  flex-shrink: 0;
  position: relative;
  font-variant-numeric: tabular-nums;
}
.ive-map-panel-rank.is-top { color: var(--brand-700); }

/* Logo tile inside the panel row — matches the table cell tile
   exactly so the panel reads like a list of the same entities the
   table presents (frameless, 36×24 with 32×20 logo content). */
.ive-map-panel-tile {
  inline-size: 36px;
  block-size: 24px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  overflow: hidden;
}
.ive-map-panel-tile > img {
  max-inline-size: 100%;
  max-block-size: 100%;
  inline-size: 32px;
  block-size: 20px;
  object-fit: contain;
  display: block;
}
.ive-map-panel-tile:not(.is-logo) {
  /* Initials fallback only — small brand-tinted pill so two-letter
     glyph reads even without a logo SVG. */
  inline-size: 28px;
  background: color-mix(in oklab, var(--brand-600) 12%, transparent);
  border-radius: 6px;
}
.ive-map-panel-tile > span {
  font-size: var(--fsz-caption);
  font-weight: 700;
  color: var(--brand-700);
  letter-spacing: 0.04em;
}

.ive-map-panel-text { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; flex: 1; }
.ive-map-panel-name {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ive-map-panel-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.ive-map-panel-sub > svg { color: var(--ink-4); flex-shrink: 0; }

/* ── Institute markers (HTML mapboxgl.Marker) ──
   Each marker = wrap > [tile, label]. The wrap anchors at the
   centroid; tile sits on top, label below. Always-on label
   adapts its content based on `.is-zoomed-in` toggle from the
   component (short name at low zoom, full name at high zoom).

   Marker style (A/B/C/D) is selected per-institute via the
   data-style attribute; CSS rules below handle the visual swap. */
.ive-map-marker-wrap {
  /* `inline-flex` so the wrap shrinks to its content (the pill +
     tail) instead of spanning the canvas width. Without this,
     wrap.offsetWidth = 1318px (full canvas) which throws off any
     offset math that uses the wrap width as a reference. */
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  transition: opacity var(--t-med);
}
/* ─── Style A — Pill-pin ─────────────────────────────── */
.ive-map-marker-wrap[data-style="a"] { gap: 0; }
/* Ground shadow — small dark ellipse below the tail tip, suggesting
   the pin is hovering above the ground. Mapbox already sets
   `position: absolute` on the wrap, so this `::after` resolves
   relative to the wrap; we MUST NOT set `position: relative` here
   ourselves — that would override Mapbox's positioning and the
   wrap would join document flow, throwing every marker out of
   place. */
.ive-map-marker-wrap[data-style="a"]::after {
  content: '';
  position: absolute;
  /* Physical `left: 50%` + `translateX(-50%)` centres the shadow
     directly under the marker. Earlier `inset-inline-start: 50%`
     resolved to `right: 50%` in RTL, which combined with the
     physical translateX(-50%) shifted the shadow off-centre to
     the left in Arabic mode. The shadow is direction-agnostic, so
     physical positioning is correct here. */
  top: 100%;
  left: 50%;
  transform: translateX(-50%) translateY(2px);
  inline-size: 18px;
  block-size: 5px;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(15, 17, 21, 0.30) 10%, transparent 75%);
  pointer-events: none;
  z-index: -1;
}
.ive-pin-a-body {
  appearance: none;
  border: 1px solid var(--line);
  /* Pill chrome follows the dashboard's raised surface so it flips
     to dark in dark mode (was `var(--bg-card, white)` which has no
     dark fallback). The logo circle inside keeps its own near-white
     surface so colourful logos stay legible against the dark pill. */
  background: var(--bg-raised);
  border-radius: 999px;
  /* Logical padding so the tight 4px gap stays on the circle side
     and the comfortable 12px gap stays on the label side, in both
     LTR and RTL. Physical `padding: 4px 12px 4px 4px` made Arabic
     labels touch the inline-end edge of the pill because the flex
     row flips in RTL but the physical padding doesn't. */
  padding-block: 4px;
  padding-inline-start: 4px;
  padding-inline-end: 12px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  box-shadow: 0 6px 18px -6px rgba(15, 17, 21, 0.34), 0 1px 0 0 rgba(15, 17, 21, 0.05);
  transition: transform var(--t-fast), box-shadow var(--t-fast), border-color var(--t-fast);
}
.ive-pin-a-body:hover { transform: translateY(-1px); border-color: color-mix(in oklab, var(--brand-600) 50%, var(--line)); }
.ive-pin-a-circle {
  inline-size: 26px;
  block-size: 26px;
  border-radius: 50%;
  /* Logo canvas — kept near-white in both modes so brand colours
     stay legible. Dark mode shifts to a slightly off-white (~94% L)
     so the canvas reads as "surface" rather than burning through
     the dark pill. Border (defined just below in dark override)
     softens the transition. */
  background: #fff;
  display: grid;
  place-items: center;
  overflow: hidden;
  flex-shrink: 0;
}
[data-theme="dark"] .ive-pin-a-circle {
  background: oklch(0.94 0 0);
  border: 1px solid color-mix(in oklab, var(--line) 50%, transparent);
}
.ive-pin-a-circle > img {
  inline-size: 22px;
  block-size: 22px;
  object-fit: contain;
}
.ive-pin-a-circle > span {
  font-weight: 700;
  font-size: var(--fsz-chart-label);
  color: var(--brand-700);
}
.ive-pin-a-name {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  letter-spacing: 0.01em;
  max-inline-size: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Same reason as `.ive-map-marker-label` — pin to the UI font so
     Arabic glyphs render in IBM Plex Sans Arabic when dir=rtl. */
  font-family: var(--font-ui);
}
.ive-pin-a-tail {
  inline-size: 0;
  block-size: 0;
  border-inline: 7px solid transparent;
  border-block-start: 9px solid var(--bg-card, white);
  margin-block-start: -1px;
  filter: drop-shadow(0 4px 4px rgba(15, 17, 21, 0.2));
  transition: filter var(--t-fast), border-block-start-color var(--t-fast);
}
/* Focus state extends to the tail too: stack three offset
   `drop-shadow`s in brand colour to outline the tail's alpha
   shape (regular box-shadow doesn't follow the triangle). The
   tail's fill is also nudged toward brand-tint so the body and
   tail read as one continuously highlighted unit. */
.ive-map-marker-wrap.is-focused .ive-pin-a-tail {
  filter:
    drop-shadow( 1.5px 0 0 color-mix(in oklab, var(--brand-600) 50%, var(--line)))
    drop-shadow(-1.5px 0 0 color-mix(in oklab, var(--brand-600) 50%, var(--line)))
    drop-shadow(0 1.5px 0 color-mix(in oklab, var(--brand-600) 50%, var(--line)))
    drop-shadow(0 4px 4px rgba(15, 17, 21, 0.22));
}

/* ─── Style B — Floating chip + ground dot ───────────── */
.ive-map-marker-wrap[data-style="b"] { gap: 0; }
.ive-pin-b-chip {
  appearance: none;
  border: 1px solid var(--line);
  background: var(--bg-card, white);
  border-radius: 999px;
  padding: 4px 12px 4px 4px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  box-shadow: 0 6px 18px -6px rgba(15, 17, 21, 0.30), 0 1px 0 0 rgba(15, 17, 21, 0.05);
  transition: transform var(--t-fast), border-color var(--t-fast);
}
.ive-pin-b-chip:hover { transform: translateY(-1px); border-color: color-mix(in oklab, var(--brand-600) 50%, var(--line)); }
.ive-pin-b-circle {
  inline-size: 24px;
  block-size: 24px;
  border-radius: 50%;
  background: var(--bg-sunken);
  display: grid;
  place-items: center;
  overflow: hidden;
  flex-shrink: 0;
}
.ive-pin-b-circle > img { inline-size: 20px; block-size: 20px; object-fit: contain; }
.ive-pin-b-circle > span { font-weight: 700; font-size: var(--fsz-chart-label); color: var(--brand-700); }
/* Branch-only fallback when the institute has no logo SVG. */
.ive-pin-b-initial {
  font-weight: 800;
  font-size: var(--fsz-label);
  line-height: 1;
  color: var(--brand-700);
  letter-spacing: 0.02em;
}
.ive-pin-b-name {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  max-inline-size: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Connector line — runs from the chip's bottom edge down to the
   ground dot. Fixed height so the chip floats consistently above
   the actual coordinate. */
.ive-pin-b-stem {
  inline-size: 1.5px;
  block-size: 18px;
  background: color-mix(in oklab, var(--ink) 35%, transparent);
}
.ive-pin-b-dot {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 50%;
  background: var(--brand-700);
  border: 2px solid var(--bg-card, white);
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--ink) 40%, transparent), 0 4px 8px -2px rgba(15, 17, 21, 0.3);
}

/* ─── Style D — Teardrop pin ─────────────────────────── */
.ive-map-marker-wrap[data-style="d"] { gap: 6px; }
.ive-pin-d-drop {
  appearance: none;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  position: relative;
  inline-size: 40px;
  block-size: 50px;
  display: block;
  filter: drop-shadow(0 6px 8px rgba(15, 17, 21, 0.32));
}
.ive-pin-d-drop::before {
  /* The teardrop body: a circle sitting on top of a triangular
     pointer. Achieved with a 40×40 circle + ::after pointer. */
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 40px;
  block-size: 40px;
  background: var(--bg-card, white);
  border: 1px solid var(--line);
  border-radius: 50%;
}
.ive-pin-d-drop::after {
  content: '';
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  inline-size: 0;
  block-size: 0;
  border-inline: 8px solid transparent;
  border-block-start: 12px solid var(--bg-card, white);
}
.ive-pin-d-head {
  position: absolute;
  inset-block-start: 5px;
  inset-inline-start: 5px;
  inline-size: 30px;
  block-size: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--bg-sunken);
  z-index: 1;
}
.ive-pin-d-head > img { inline-size: 26px; block-size: 26px; object-fit: contain; }
.ive-pin-d-head > span { font-weight: 700; font-size: var(--fsz-caption); color: var(--brand-700); }
.ive-pin-d-drop:hover { transform: translateY(-2px); }
.ive-pin-d-drop:hover::before { border-color: color-mix(in oklab, var(--brand-600) 60%, var(--line)); }
/* Marker focus / tint state classes.
   `is-tinted` — applied to OTHER institute markers when one is
   hovered or pinned. Uses a grayscale + slight contrast filter so
   the icon and label desaturate to read as background context
   without disappearing or fighting the focused marker.
   `is-focused` — applied to the marker the user is on; subtle ring
   so the selection is unambiguous, no bg colour change.
   Drill-mode `is-hidden` (used only for the SELECTED institute
   marker, which is replaced by branch markers) keeps display:none. */
/* Tint = fade only the CONTENT (logo image + label text), not the
   container chrome. Keeps the white tile and label pill at full
   strength so the markers' shape still reads cleanly while the
   colours/labels recede. Applied only when another marker is the
   focus (hover or pinned). Per-style selectors so all 4 candidate
   marker designs (A/B/C/D) get the same treatment. */
.ive-map-marker-wrap.is-tinted .ive-map-marker > img,
.ive-map-marker-wrap.is-tinted .ive-pin-a-circle > img,
.ive-map-marker-wrap.is-tinted .ive-pin-b-circle > img,
.ive-map-marker-wrap.is-tinted .ive-pin-d-head   > img,
.ive-branch-marker-wrap.is-tinted .ive-pin-a-circle > img {
  opacity: 0.35;
  filter: grayscale(0.85);
  transition: opacity var(--t-med), filter var(--t-med);
}
.ive-map-marker-wrap.is-tinted .ive-map-marker > span,
.ive-map-marker-wrap.is-tinted .ive-pin-a-circle > span,
.ive-map-marker-wrap.is-tinted .ive-pin-b-circle > span,
.ive-map-marker-wrap.is-tinted .ive-pin-d-head   > span,
.ive-branch-marker-wrap.is-tinted .ive-pin-a-circle > span {
  opacity: 0.45;
  transition: opacity var(--t-med);
}
.ive-map-marker-wrap.is-tinted .ive-map-marker-label,
.ive-map-marker-wrap.is-tinted .ive-pin-a-name,
.ive-map-marker-wrap.is-tinted .ive-pin-b-name,
.ive-branch-marker-wrap.is-tinted .ive-pin-a-name {
  color: color-mix(in oklab, var(--ink) 35%, transparent);
  transition: color var(--t-med);
}
/* B's connector + dot also fade for consistency. */
.ive-map-marker-wrap[data-style="b"].is-tinted .ive-pin-b-stem,
.ive-map-marker-wrap[data-style="b"].is-tinted .ive-pin-b-dot {
  opacity: 0.45;
}
.ive-map-marker-wrap.is-hidden,
.ive-branch-marker-wrap.is-hidden,
.ive-map-marker-wrap.is-faded /* legacy alias kept for safety */ { display: none !important; }
.ive-map-marker-wrap.is-focused,
.ive-branch-marker-wrap.is-focused { z-index: 2; }
/* Focus rings — applied to whichever element is the visible
   marker body for each style. */
.ive-map-marker-wrap.is-focused .ive-map-marker,
.ive-map-marker-wrap.is-focused .ive-pin-a-body,
.ive-map-marker-wrap.is-focused .ive-pin-b-chip,
.ive-branch-marker-wrap.is-focused .ive-pin-a-body {
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--brand-600) 35%, transparent),
              0 6px 18px -6px rgba(15, 17, 21, 0.32);
  border-color: color-mix(in oklab, var(--brand-600) 50%, var(--line));
}
/* C/D — also ring the label pill so the marker reads as one unit. */
.ive-map-marker-wrap.is-focused .ive-map-marker-label {
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--brand-600) 35%, transparent),
              0 4px 12px -6px rgba(15, 17, 21, 0.22);
  border-color: color-mix(in oklab, var(--brand-600) 50%, var(--line));
}
/* D — outline the teardrop body's circle on focus. */
.ive-map-marker-wrap[data-style="d"].is-focused .ive-pin-d-drop::before {
  border-color: color-mix(in oklab, var(--brand-600) 50%, var(--line));
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--brand-600) 35%, transparent);
}
.ive-branch-marker-wrap { transition: opacity var(--t-med); }
.ive-map-marker {
  appearance: none;
  /* Theme-aware surface — `--d-bg-card` is defined on the dashboard
     scope (.app:has(.dash-header)) for both light and dark modes.
     The previous `var(--bg-card, white)` hardcoded white as the
     fallback, which made the markers stay white in dark mode (where
     --bg-card is undefined outside .hp-shell/.sm-shell). */
  border: 1px solid var(--d-line, var(--line));
  background: var(--d-bg-card, var(--bg-raised, white));
  border-radius: 8px;
  /* 44×32 frame with 4px padding all around → 36×24 logo content
     (matches the user-requested 4px margin on all sides). */
  inline-size: 44px;
  block-size: 32px;
  padding: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  /* Clip the inner img to the box. Without overflow:hidden, SVG
     logos with non-zero internal padding can extrude past the
     rounded border. */
  overflow: hidden;
  box-shadow: 0 4px 14px -6px rgba(15, 17, 21, 0.30), 0 1px 0 0 rgba(15, 17, 21, 0.05);
  transition: transform var(--t-fast), box-shadow var(--t-fast), border-color var(--t-fast);
  box-sizing: border-box;
}
.ive-map-marker > img {
  /* Fill the 36×24 content area; object-fit:contain preserves
     aspect so wider wordmarks bind to width and taller monograms
     bind to height. */
  max-inline-size: 100%;
  max-block-size: 100%;
  inline-size: 36px;
  block-size: 24px;
  object-fit: contain;
  display: block;
}
.ive-map-marker > span {
  font-weight: 700;
  font-size: var(--fsz-caption);
  color: var(--ink);
  letter-spacing: 0.04em;
}
.ive-map-marker:hover {
  transform: translateY(-1px);
  border-color: color-mix(in oklab, var(--brand-600) 60%, var(--line));
  box-shadow: 0 6px 18px -6px rgba(15, 17, 21, 0.34), 0 0 0 2px color-mix(in oklab, var(--brand-600) 30%, transparent);
}
[data-theme="dark"] .ive-map-marker {
  background: oklch(0.97 0.005 250);
}

/* Always-on name label below the tile. Two child spans — only one
   visible at a time per zoom state. */
.ive-map-marker-label {
  display: inline-block;
  padding: 2px 7px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  box-shadow: 0 3px 10px -6px rgba(15, 17, 21, 0.22);
  max-inline-size: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Mapbox marker wrappers are appended to the map container with
     no inherited typography context; explicitly use the project's
     UI font so Arabic labels render in IBM Plex Sans Arabic and
     match the rest of the dashboard. */
  font-family: var(--font-ui);
}
.ive-map-marker-label-short { display: inline; }
.ive-map-marker-label-full  { display: none; }
.ive-map-marker-wrap.is-zoomed-in .ive-map-marker-label-short { display: none; }
.ive-map-marker-wrap.is-zoomed-in .ive-map-marker-label-full  { display: inline; }

/* Live tick chip — floating counter that drifts upward when an
   institute's tests count just bumped (live snap update). */
.ive-map-marker-tick {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: -2px;
  transform: translate(-50%, 0);
  padding: 2px 7px;
  background: var(--brand-600);
  color: white;
  border-radius: var(--r-pill);
  font-size: var(--fsz-caption);
  font-weight: 700;
  letter-spacing: 0.02em;
  pointer-events: none;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 4px 12px -4px color-mix(in oklab, var(--brand-600) 60%, transparent);
  animation: ive-map-tick 1.5s ease-out forwards;
  z-index: 2;
}
@keyframes ive-map-tick {
  0%   { opacity: 0; transform: translate(-50%, 4px) scale(0.9); }
  15%  { opacity: 1; transform: translate(-50%, -10px) scale(1); }
  85%  { opacity: 1; transform: translate(-50%, -28px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -36px) scale(1); }
}
.ive-map-marker-wrap { position: relative; }

/* ── Branch markers (drill mode) ──
   Same family as institute markers but slightly larger tile and
   wider label allowance (branch + institute prefix). */
/* Branch marker wrap — uses the Style A pill-pin markup
   (`.ive-pin-a-body / -circle / -name / -tail`) shared with the
   institute markers, so the visual language is identical at L1
   and during drill. Wrap CSS mirrors `.ive-map-marker-wrap[data-
   style="a"]` (inline-flex column, ground shadow via ::after).
   `position: relative` is NOT set here — Mapbox sets
   `position: absolute` on the wrap, and overriding it would
   throw markers out of place. */
.ive-branch-marker-wrap {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  transition: opacity var(--t-med);
}
.ive-branch-marker-wrap::after {
  content: '';
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 50%;
  transform: translateX(-50%) translateY(2px);
  inline-size: 18px;
  block-size: 5px;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(15, 17, 21, 0.30) 10%, transparent 75%);
  pointer-events: none;
  z-index: -1;
}
/* Zoom-aware label swap (short at low zoom, full at high zoom)
   reuses the same `.ive-map-marker-label-short / -full` toggle
   as the institute markers, so a single rule covers both. */

/* ── Unified hover/pin card (institute or branch) ──
   Layout mirrors the InstituteCard: head with tile + name + sub,
   hero number, multi-seg pass-rate bar with caption, stats grid,
   and an optional CTA visible only when pinned. */
/* All `.ive-card*` rules below are scoped under `.ive-map-wrap`
   because `.ive-card` is also a generic card primitive defined in
   `dashboard.css` (used by the mobility dashboard). Without the
   scope, the bare `.ive-card { inline-size: 280px; ... }` rule
   leaks into every `.ive-card` on the mobility page and forces
   them all to 280px wide regardless of grid cell. The map's
   floating institute hover/pinned cards live exclusively inside
   `.ive-map-wrap`, so this scope is exact. */
.ive-map-wrap .ive-card-anchor {
  position: absolute;
  z-index: 6;
  pointer-events: auto;
}
.ive-map-wrap .ive-card {
  inline-size: 280px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 14px;
  box-shadow: 0 18px 40px -14px rgba(15, 17, 21, 0.32), 0 1px 0 0 rgba(15, 17, 21, 0.04);
  display: flex;
  flex-direction: column;
  gap: 12px;
  animation: ive-card-in 140ms ease-out;
  position: relative;
}
@keyframes ive-card-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.ive-map-wrap .ive-card.is-pinned { box-shadow: 0 22px 48px -12px rgba(15, 17, 21, 0.40), 0 0 0 1px color-mix(in oklab, var(--brand-600) 25%, var(--line)); }
.ive-map-wrap .ive-card-close {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
  inline-size: 26px;
  block-size: 26px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-3);
  border-radius: var(--r-xs);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
  padding: 0;
}
.ive-map-wrap .ive-card-close > svg { display: block; }
.ive-map-wrap .ive-card-close:hover { color: var(--ink); background: color-mix(in oklab, var(--brand-600) 8%, transparent); border-color: var(--line); }
.ive-map-wrap .ive-card-head { display: flex; align-items: center; gap: 12px; }
.ive-map-wrap .ive-card-tile {
  inline-size: 40px;
  block-size: 40px;
  border-radius: 10px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  font-weight: 700;
  font-size: var(--fsz-body);
  color: white;
  background: var(--brand-600);
  overflow: hidden;
}
.ive-map-wrap .ive-card-tile.is-logo {
  background: transparent;
  border: 1px solid var(--line);
  padding: 8px 4px;
}
.ive-map-wrap .ive-card-tile > img {
  inline-size: 32px;
  block-size: 24px;
  object-fit: contain;
  display: block;
}
[data-theme="dark"] .ive-map-wrap .ive-card-tile.is-logo {
  /* Map institute hover/click card — same near-white canvas pattern
     as the list/card-view tiles so colourful logos stay legible
     against the dark card chrome. */
  background: oklch(0.94 0 0);
  border-color: color-mix(in oklab, var(--line) 50%, transparent);
}
.ive-map-wrap .ive-card-id { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.ive-map-wrap .ive-card-name {
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
}
.ive-map-wrap .ive-card-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.ive-map-wrap .ive-card-sub > svg { color: var(--ink-4); flex-shrink: 0; }
.ive-map-wrap .ive-card-hero {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.ive-map-wrap .ive-card-hero-num {
  font-size: var(--fsz-display); /* snapped from 26 to display (28) — on-scale */
  font-weight: 700;
  color: var(--ink);
  line-height: 1;
}
.ive-map-wrap .ive-card-hero-label {
  font-size: var(--fsz-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
.ive-map-wrap .ive-card-bar {
  block-size: 6px;
  display: flex;
  gap: 2px;
}
.ive-map-wrap .ive-card-bar-seg {
  display: block;
  block-size: 6px;
  border-radius: var(--r-pill);
  transition: inline-size 280ms ease, background 180ms ease;
}
/* Subdued at-rest, bold on card hover — same grammar as the rate
   bar in the InstituteCard. */
.ive-map-wrap .ive-card-bar-seg.is-pass    { background: var(--pass); }
.ive-map-wrap .ive-card-bar-seg.is-fail    { background: var(--fail-border); }
.ive-map-wrap .ive-card-bar-seg.is-absent  { background: var(--absent-border); }
.ive-map-wrap .ive-card-bar-seg.is-pending { background: var(--pending-border); }
.ive-map-wrap .ive-card:hover .ive-card-bar-seg.is-fail    { background: var(--fail); }
.ive-map-wrap .ive-card:hover .ive-card-bar-seg.is-absent  { background: var(--absent); }
.ive-map-wrap .ive-card:hover .ive-card-bar-seg.is-pending { background: var(--pending); }
.ive-map-wrap .ive-card-bar-cap {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  display: flex;
  align-items: baseline;
  gap: 4px;
  line-height: 1;
}
.ive-map-wrap .ive-card-bar-cap > .tnum { color: var(--ink); font-weight: 600; }
.ive-map-wrap .ive-card-bar-cap-label {
  font-size: var(--fsz-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
.ive-map-wrap .ive-card-stats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 10px;
}
.ive-map-wrap .ive-card-stat { display: flex; flex-direction: column; gap: 2px; }
/* Card stat labels (Examiners / Vehicles / Contribution) match the
   `ive-card-bar-cap-label` style above (caption size, 0.06em
   tracking) so the typography rhythm reads as one stack. */
.ive-map-wrap .ive-card-stat-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  font-family: var(--font-ui);
}
/* Stat numbers — pulled down a step from h3 so the row reads as
   secondary data rather than competing with the hero number above.
   Same weight + colour as the bar-cap's `.tnum` so the eye groups
   them as the same "supporting data" rank. */
.ive-map-wrap .ive-card-stat-num {
  font-size: var(--fsz-body-strong, var(--fsz-body));
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
  font-family: var(--font-ui);
  font-variant-numeric: tabular-nums;
}
/* CTA — text-link style: no box, no fill, just brand-coloured
   text + chevron. Hover underlines the text and shifts the
   chevron forward. Reads as a quiet inline action rather than
   a primary button. */
.ive-map-wrap .ive-card-cta {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--brand-700);
  padding: 4px 0;
  font-size: var(--fsz-label);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  letter-spacing: 0.01em;
  transition: color var(--t-fast), gap var(--t-fast);
  align-self: flex-start;
}
.ive-map-wrap .ive-card-cta:hover {
  color: var(--brand-800, var(--brand-700));
  gap: 6px;
}
/* Unified inline-link standard — no underline on hover; the arrow
   icon's translateX (see rule below) is the hover affordance, with
   the parent's color-darken already covering the color shift. */
.ive-map-wrap .ive-card-cta > svg { color: inherit; transition: transform var(--t-fast); }
.ive-map-wrap .ive-card-cta:hover > svg { transform: translateX(2px); }
[dir="rtl"] .ive-map-wrap .ive-card-cta svg { transform: scaleX(-1); }
[dir="rtl"] .ive-map-wrap .ive-card-cta:hover > svg { transform: scaleX(-1) translateX(2px); }

/* ── Pass-rate bar with hover-driven breakdown popover ── */
.ive-map-wrap .ive-card-bar-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-block: 6px;
  cursor: default;
}
.ive-map-wrap .ive-card-bar-wrap:hover .ive-card-bar-seg.is-fail,
.ive-map-wrap .ive-card-bar-wrap:focus-within .ive-card-bar-seg.is-fail   { background: var(--fail); }
.ive-map-wrap .ive-card-bar-wrap:hover .ive-card-bar-seg.is-absent,
.ive-map-wrap .ive-card-bar-wrap:focus-within .ive-card-bar-seg.is-absent { background: var(--absent); }
.ive-map-wrap .ive-card-bar-wrap:hover .ive-card-bar-seg.is-pending,
.ive-map-wrap .ive-card-bar-wrap:focus-within .ive-card-bar-seg.is-pending { background: var(--pending); }
.ive-map-wrap .ive-card-bar-pop {
  position: absolute;
  inset-block-end: calc(100% + 4px);
  inset-inline-end: 0;
  inline-size: max-content;
  min-inline-size: 220px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: 0 8px 22px -8px rgba(15, 17, 21, 0.22), 0 1px 0 0 rgba(15, 17, 21, 0.04);
  padding: 8px 10px;
  display: none;
  flex-direction: column;
  gap: 4px;
  z-index: 10;
  pointer-events: none;
  font-size: var(--fsz-label);
}
.ive-map-wrap .ive-card-bar-wrap:hover .ive-card-bar-pop,
.ive-map-wrap .ive-card-bar-wrap:focus-within .ive-card-bar-pop { display: flex; }
.ive-map-wrap .ive-card-bar-pop-row {
  display: grid;
  grid-template-columns: 8px 1fr auto auto;
  align-items: center;
  gap: 8px;
  font-variant-numeric: tabular-nums;
}
.ive-map-wrap .ive-card-bar-pop-dot { inline-size: 8px; block-size: 8px; border-radius: 50%; }
.ive-map-wrap .ive-card-bar-pop-row.is-pass    .ive-card-bar-pop-dot { background: var(--pass); }
.ive-map-wrap .ive-card-bar-pop-row.is-fail    .ive-card-bar-pop-dot { background: var(--fail); }
.ive-map-wrap .ive-card-bar-pop-row.is-absent  .ive-card-bar-pop-dot { background: var(--absent); }
.ive-map-wrap .ive-card-bar-pop-row.is-pending .ive-card-bar-pop-dot { background: var(--pending); }
.ive-map-wrap .ive-card-bar-pop-label { color: var(--ink-2, var(--ink)); font-size: var(--fsz-caption); }
.ive-map-wrap .ive-card-bar-pop-num   { color: var(--ink); font-weight: 600; font-size: var(--fsz-label); }
.ive-map-wrap .ive-card-bar-pop-pct   { color: var(--ink-3); font-size: var(--fsz-caption); min-inline-size: 42px; text-align: end; }

/* ── Custom Mapbox IControl buttons (basemap + default view) ──
   These mount inside the .mapboxgl-ctrl-top-right flex column as
   real Mapbox controls, so they share the column's chrome and
   sizing with the nav + fullscreen groups (single visual stack,
   single button size, no absolute-positioning math). */
.ive-map-wrap .ive-ctrl-group { /* same group treatment via parent rule already */ }
.ive-map-wrap .ive-ctrl-basemap,
.ive-map-wrap .ive-ctrl-default-view {
  background-position: center;
  background-repeat: no-repeat;
  background-size: 18px 18px;
  position: relative;
}
/* Stacked-layers icon (mobility-style) for the basemap button.
   Inline data-URI so we don't need an asset round-trip. */
.ive-map-wrap .ive-ctrl-basemap {
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12,2 22,8 12,14 2,8'/%3E%3Cpolyline points='2,16 12,22 22,16'/%3E%3Cpolyline points='2,12 12,18 22,12'/%3E%3C/svg%3E");
}
.ive-map-wrap .ive-ctrl-basemap[data-basemap="satellite"] {
  background-color: color-mix(in oklab, var(--brand-600) 12%, transparent);
}
/* Reset glyph (counter-clockwise rotating arrow) for the
   default-view button — reads as "reset to default" / "go back to
   how it was", more action-oriented than the my-location target. */
.ive-map-wrap .ive-ctrl-default-view {
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12a9 9 0 1 0 3-6.7L3 8'/%3E%3Cpath d='M3 3v5h5'/%3E%3C/svg%3E");
}
[data-theme="dark"] .ive-map-wrap .ive-ctrl-basemap,
[data-theme="dark"] .ive-map-wrap .ive-ctrl-default-view { filter: invert(0.92); }
/* Hide the "Reset view" control's whole group while the camera is
   already at default. The group element is a sibling-of-sibling so
   we have to walk to the parent .mapboxgl-ctrl-group that contains
   the button. Targeting the button + parent via :has() keeps the
   selector local to this control. */
.ive-map-wrap.is-at-default .mapboxgl-ctrl-group:has(.ive-ctrl-default-view) {
  display: none;
}
/* Disable click-through on Mapbox attribution chrome (bottom-left
   logo link + bottom-right compact attribution toggle). The user
   only sees them as a thin shadow against the map and they pull
   focus + open external links on accidental clicks. Visual is
   preserved (`visibility` stays); only pointer events are off. */
.ive-map-wrap .mapboxgl-ctrl-attrib,
.ive-map-wrap .mapboxgl-ctrl-attrib-button,
.ive-map-wrap .mapboxgl-ctrl-attrib a,
.ive-map-wrap .mapboxgl-ctrl-logo {
  pointer-events: none;
  cursor: default;
}

/* ── Fullscreen Time-range pill ─────────────────────────────── */
.ive-map-wrap .ive-map-fs-range {
  position: absolute;
  top: 10px;
  /* Clear the Mapbox top-right control column (~36px) plus an
     8px gap so the two sit side-by-side without touching. */
  inset-inline-end: 56px;
  z-index: 50;
}
.ive-map-wrap .ive-map-fs-range-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  block-size: 32px;
  border-radius: 999px;
  border: 1px solid var(--d-line);
  background: var(--d-bg-card);
  color: var(--d-ink-2);
  font-size: var(--fsz-label);
  font-weight: 500;
  cursor: pointer;
  box-shadow: var(--d-sh-card);
  /* Resting opacity matches `.mapboxgl-ctrl-group`. The wrap toggles
     `.is-active` on mouse movement to bring everything to full
     opacity together — see `.ive-map-wrap.is-active` rules below. */
  opacity: 0.55;
  transition: opacity 200ms ease, background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.ive-map-wrap .ive-map-fs-range-btn:focus-visible {
  background: color-mix(in oklab, var(--d-indigo) 8%, var(--d-bg-card));
  color: var(--d-ink);
}
.ive-map-wrap .ive-map-fs-range-btn.is-open {
  background: var(--d-indigo);
  color: var(--brand-ink, #fff);
  border-color: var(--d-indigo);
}
.ive-map-wrap .ive-map-fs-range-btn.is-open {
  background: var(--d-indigo);
  color: var(--brand-ink, #fff);
  border-color: var(--d-indigo);
}

/* ── Map-wide activity fade ──────────────────────────────────
   Resting state: every CTA + map control sits at 0.55 opacity.
   When the wrap has `.is-active` (set by JS on mousemove, cleared
   after 10s of idle) everything snaps to full opacity together.
   Mapbox's own ctrl chrome doesn't fade by default, so we drive
   its resting opacity from this rule too. */
.ive-map-wrap .ive-map-panel-open,
.ive-map-wrap .ive-map-fs-range-btn,
.ive-map-wrap .ive-map-fs-sum-toggle,
.ive-map-wrap .mapboxgl-ctrl-top-right .mapboxgl-ctrl,
.ive-map-wrap .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl,
.ive-map-wrap .mapboxgl-ctrl-bottom-left  .mapboxgl-ctrl {
  opacity: 0.55;
  transition: opacity 200ms ease, background-color 120ms ease, color 120ms ease, border-color 120ms ease, transform var(--t-fast);
}
.ive-map-wrap.is-active .ive-map-panel-open,
.ive-map-wrap.is-active .ive-map-fs-range-btn,
.ive-map-wrap.is-active .ive-map-fs-sum-toggle,
.ive-map-wrap.is-active .ive-map-panel,
.ive-map-wrap.is-active .mapboxgl-ctrl-top-right .mapboxgl-ctrl,
.ive-map-wrap.is-active .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl,
.ive-map-wrap.is-active .mapboxgl-ctrl-bottom-left  .mapboxgl-ctrl {
  opacity: 1;
}
/* Always-on while a panel is open or the menu is showing — fading
   an already-open menu would feel buggy. */
.ive-map-wrap.is-panel-open .ive-map-panel-open,
.ive-map-wrap .ive-map-fs-range-btn.is-open,
.ive-map-wrap .ive-map-fs-sum-toggle.is-open {
  opacity: 1;
}

/* Suppress in-map data-tip tooltips while the map is NOT in
   interactive mode — the user hasn't engaged yet, so popping
   tooltips on casual cursor passes adds noise. Exception: the
   map controls (Mapbox built-ins + our custom basemap/default-view
   IControls + the panel-open pill) ARE UI affordances and should
   always reveal their tooltip on hover so users understand what
   they do before clicking to engage. */
.ive-map-wrap:not(.is-interactive) [data-tip]:hover::after,
.ive-map-wrap:not(.is-interactive) [data-tip]:focus-visible::after,
.ive-map-wrap:not(.is-interactive) [data-tip]:hover::before,
.ive-map-wrap:not(.is-interactive) [data-tip]:focus-visible::before {
  opacity: 0;
}
/* Re-allow tooltips for controls regardless of interactive state. */
.ive-map-wrap .mapboxgl-ctrl-group [data-tip]:hover::after,
.ive-map-wrap .mapboxgl-ctrl-group [data-tip]:focus-visible::after,
.ive-map-wrap .ive-map-panel-open[data-tip]:hover::after,
.ive-map-wrap .ive-map-panel-open[data-tip]:focus-visible::after,
.ive-map-wrap .ive-map-fs-range-btn[data-tip]:hover::after,
.ive-map-wrap .ive-map-fs-sum-toggle[data-tip]:hover::after {
  opacity: 1;
}

/* Live pulsing dot inside the range pill (today only) — sized to
   match the calendar SVG (14×14) it replaces so the pill width
   doesn't shift between "Today" and other ranges. */
.ive-map-wrap .ive-map-fs-range-btn .ive-map-fs-range-live {
  inline-size: 8px;
  block-size: 8px;
  margin-inline-end: 2px;
}
.ive-map-wrap .ive-map-fs-range-menu {
  position: absolute;
  top: 38px;
  inset-inline-end: 0;
  inline-size: 180px;
  list-style: none;
  margin: 0;
  padding: 4px 0;
  background: color-mix(in oklab, var(--d-bg-card) 96%, transparent);
  backdrop-filter: blur(8px);
  border: 1px solid var(--d-line);
  border-radius: 12px;
  box-shadow: var(--d-sh-card);
}
.ive-map-wrap .ive-map-fs-range-item {
  display: block;
  inline-size: 100%;
  text-align: start;
  padding: 8px 12px;
  border: 0;
  background: transparent;
  color: var(--d-ink-2);
  font-size: var(--fsz-label);
  cursor: pointer;
}
.ive-map-wrap .ive-map-fs-range-item:hover {
  background: color-mix(in oklab, var(--d-indigo) 8%, transparent);
  color: var(--d-ink);
}
.ive-map-wrap .ive-map-fs-range-item.is-active {
  color: var(--d-indigo);
  font-weight: 600;
}

/* ── Fullscreen Summary overlay ─────────────────────────────── */
.ive-map-wrap .ive-map-fs-sum-toggle {
  position: absolute;
  bottom: 16px;
  inset-inline-start: 16px;
  z-index: 50;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px;
  block-size: 32px;
  border-radius: 999px;
  border: 1px solid var(--d-line);
  background: var(--d-bg-card);
  color: var(--d-ink-2);
  font-size: var(--fsz-label);
  font-weight: 500;
  cursor: pointer;
  box-shadow: var(--d-sh-card);
  opacity: 0.55;
  transition: opacity 120ms ease, background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.ive-map-wrap .ive-map-fs-sum-toggle:focus-visible {
  background: color-mix(in oklab, var(--d-indigo) 8%, var(--d-bg-card));
  color: var(--d-ink);
}
.ive-map-wrap .ive-map-fs-sum-toggle.is-open {
  background: var(--d-indigo);
  color: var(--brand-ink, #fff);
  border-color: var(--d-indigo);
}
.ive-map-wrap .ive-map-fs-sum-panel {
  position: absolute;
  bottom: 60px;
  inset-inline-start: 16px;
  z-index: 49;
  /* Widened to fit the longer Arabic labels (e.g. "اختبارات تعديل
     النتيجة" and "اختبارات أول محاولة" need ~360px to render in
     full) without truncating. Capped to viewport width minus margin
     so it still fits on small screens. */
  inline-size: min(380px, calc(100vw - 32px));
  background: color-mix(in oklab, var(--d-bg-card) 96%, transparent);
  backdrop-filter: blur(8px);
  border: 1px solid var(--d-line);
  border-radius: var(--d-r-card, 14px);
  box-shadow: var(--d-sh-card);
  overflow: hidden;
}
.ive-map-fs-sum-head {
  padding: 10px 14px;
  border-block-end: 1px solid var(--d-line);
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--d-ink-3);
}
.ive-map-fs-sum-list {
  list-style: none;
  margin: 0;
  padding: 6px 0;
}
.ive-map-fs-sum-row {
  display: grid;
  grid-template-columns: 8px 1fr auto auto;
  align-items: baseline;
  gap: 8px;
  padding: 7px 14px;
  font-size: var(--fsz-label);
}
.ive-map-fs-sum-row + .ive-map-fs-sum-row { border-block-start: 1px solid color-mix(in oklab, var(--d-line) 60%, transparent); }
.ive-map-fs-sum-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  align-self: center;
}
.ive-map-fs-sum-label {
  color: var(--d-ink-2);
  /* Allow the label to wrap if it can't fit on one line (Arabic
     labels like "النجاح من أول محاولة" are notably wider than
     their English equivalents). Truncate only as a last resort. */
  white-space: normal;
  overflow: hidden;
  text-overflow: ellipsis;
  word-break: keep-all;
}
.ive-map-fs-sum-hero  { color: var(--d-ink); font-weight: 600; }
.ive-map-fs-sum-sub   { color: var(--d-ink-3); font-size: var(--fsz-caption); }

/* ════════════════════════════════════════════════════════════════
   TIER 2/3 LAYOUT — sidebar + main shell
   ════════════════════════════════════════════════════════════════ */
.d2-shell {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 16px;
  /* No margin: the parent flex container's `gap` provides spacing.
     Margin would stack on top of it and create a different visual
     gap than the rest of the page. */
  margin-block-start: 0;
  align-items: flex-start;
}
/* Make the dashboard's vertical rhythm consistent — same 16px gap
   between every section (header → first section, and section →
   section inside d2-main). Matches `.d2-main { gap: 16px }`. */
.app:has(.dash-header) main.main--dash { gap: 16px; }

/* ── Independent column scroll for L2/L3 dashboards ──
   The two columns of `.d2-shell` (sidebar digests + main content)
   each get their own scroll axis WHEN there's enough width to show
   both side-by-side. Below the breakpoint (≤1100px) the shell
   collapses to one column — at that point we drop the per-column
   scroll lock and revert to natural page-level scrolling.
   Outside this scope (e.g. the L1 "All Institutes" page, which has
   no `.d2-shell`), scrolling stays page-level too. */
@media (min-width: 1101px) {
  html:has(.d2-shell), body:has(.d2-shell) {
    block-size: 100vh;
    overflow: hidden;
  }
  /* On T2/T3 desktop, the .dash-header sits outside .d2-main (the
     scrolling column), so it's always visible — the compact strip
     would just duplicate it. Hide on those pages at ≥1101px; T1 and
     savedtests still get the strip because they use page-level scroll. */
  .app:has(.d2-shell) .compact-strip {
    display: none;
  }
  .app:has(.d2-shell) {
    block-size: 100vh;
    min-height: 0;
    overflow: hidden;
    /* Explicit single-row grid (minmax(0, 1fr)) forces main to fill
       the locked-100vh app cell instead of sizing to its intrinsic
       content (which broke the .d2-shell flex:1 + .d2-side/.d2-main
       overflow-y:auto chain — d2-main ended up the same height as
       its content, no scroll). minmax(0, …) lets the row shrink
       below content's intrinsic min-size so children can scroll. */
    grid-template-rows: minmax(0, 1fr);
  }
  .app:has(.d2-shell) main.main--dash {
    min-block-size: 0;
    overflow: hidden;
    /* Trim the bottom padding inside the locked viewport — without
       a page-level scroll, the 48px tail just consumes column
       height for nothing. */
    padding-block-end: 16px;
  }
  .app:has(.d2-shell) .d2-shell {
    min-block-size: 0;
    flex: 1;
    overflow: hidden;
  }
  .app:has(.d2-shell) .d2-side {
    /* Drop the sticky positioning — each column owns its own
       scrollbar now, sticky is redundant and would clip oddly. */
    position: static;
    block-size: 100%;
    min-block-size: 0;
    overflow-y: auto;
    padding-inline-end: 4px;
  }
  .app:has(.d2-shell) .d2-main {
    block-size: 100%;
    min-block-size: 0;
    overflow-y: auto;
    padding-inline-end: 4px;
  }
}
/* Allow grid children to shrink below their intrinsic min-content
   width — without this, the widest descendant (tables, fleet rows,
   maneuver grid) forces the whole shell wider than the viewport on
   small screens, even though grid-template-columns is `1fr`. */
.d2-shell > * { min-inline-size: 0; }
/* On narrow viewports, allow the wide tier-2/3 tables (examiners,
   fleet detail, branch comparison, top-ten) to scroll horizontally
   inside their card rather than push the page wider than the viewport. */
@media (max-width: 720px) {
  .d2-exam-tbl,
  .d2-fleet-detail,
  .branch-cmp-tbl-wrap,
  .d2-toptens .d2-top10 { overflow-x: auto; }
}
@media (max-width: 1100px) {
  .d2-shell { grid-template-columns: 1fr; }
}

/* ── Digest sidebar ──
   Grid layout so the digest cards can flow into multiple columns at
   narrow viewports (where the d2-shell collapses to a single column
   and the sidebar takes the full content width). At desktop the
   sidebar is the 280px sticky rail and 1fr keeps it as a single
   column. */
.d2-side {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  position: sticky;
  top: 12px;
  /* Pack rows from the start — without this, the grid's default
     stretch behaviour distributes the side's leftover height
     evenly across rows on tall screens, leaving each digest card
     with a chunk of dead space below its content. */
  align-content: start;
}
/* Below the d2-shell collapse (≤1100px) the d2-side is full-width.
   Switch to CSS multi-column layout so cards flow with browser-
   balanced column heights — packing shorter cards alongside
   taller ones instead of locking each row to the tallest item's
   height (which was leaving Requests / Feedback with a big gap
   below when Appointments was the tallest in row 1).
   `columns: 280px` lets the browser pick the column count from
   the available width: ~3 cols at 1100, ~2 at 720, 1 at 480. */
@media (max-width: 1100px) {
  .d2-side {
    position: static;
    display: block;
    grid-template-columns: none;
    columns: 280px;
    column-gap: 12px;
    /* The base .d2-side rule sets gap: 12px which is ignored by
       multi-column layout — give each card its own bottom margin
       to space them within a column. */
  }
  .d2-side > .d2-digest {
    break-inside: avoid;
    margin-block-end: 12px;
    /* Ensure the card box renders cleanly inside the column flow. */
    inline-size: 100%;
  }
  .d2-side > .d2-digest:last-child { margin-block-end: 0; }
}
.d2-digest {
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  padding: 14px 16px;
  box-shadow: var(--d-sh-card);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.d2-digest-head {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--d-ink);
}
.d2-digest-title {
  margin: 0;
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--d-ink);
  flex: 1;
}
.d2-digest-link {
  font-size: var(--fsz-caption);
  color: var(--brand-700);
  text-decoration: none;
  font-weight: 600;
  /* Unified inline-link treatment — color darken + soft brand bg
     tint on hover, never underline. Negative margin preserves the
     layout footprint inside the digest header row. */
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.d2-digest-link:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.d2-digest-link:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
.d2-digest-body { display: flex; flex-direction: column; gap: 8px; }
.d2-digest-hero {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding-block-end: 4px;
  border-block-end: 1px solid var(--d-line-soft);
}
.d2-digest-hero-num {
  /* Unified to 22px to match the .d2-fleet-stat-num and
     .d2-digest-rating-num scales. Was 30 — felt out of place
     against the smaller hero numbers in vehicles + feedback. */
  font-size: var(--fsz-h1);
  font-weight: 700;
  color: var(--d-ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.d2-digest-hero-label {
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
}
.d2-digest-rows { display: flex; flex-direction: column; gap: 4px; }
.d2-digest-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-label);
  padding: 3px 0;
  border-block-end: 1px dashed var(--d-line-soft);
}
.d2-digest-sub-row { padding-inline-start: 18px; border-block-end: 0; color: var(--d-ink-4); font-size: var(--fsz-caption); }
.d2-digest-row:last-child { border-block-end: 0; }
.d2-digest-row-icon { color: var(--d-ink-4); display: inline-flex; }
.d2-digest-row.is-ok   .d2-digest-row-icon { color: var(--d-ok); }
.d2-digest-row.is-err  .d2-digest-row-icon { color: var(--d-err); }
.d2-digest-row.is-warn .d2-digest-row-icon { color: var(--d-warn); }
.d2-digest-row-label { flex: 1; color: var(--d-ink-2); }
.d2-digest-row-value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--d-ink);
}
.d2-digest-row.is-ok .d2-digest-row-value   { color: var(--d-ok); }
.d2-digest-row.is-err .d2-digest-row-value  { color: var(--d-err); }
.d2-digest-row.is-warn .d2-digest-row-value { color: var(--d-warn); }
.d2-digest-foot { display: flex; flex-direction: column; gap: 4px; padding-block-start: 12px; border-block-start: 1px solid var(--d-line-soft); }
.d2-digest-foot-label { font-size: var(--fsz-caption); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--d-ink-3); }
/* (Old .d2-genderbar / .d2-dot rules removed — DigestAppointments
   now renders the shared <GenderSplitBar>, which uses .kpi-genderbar*. */
.d2-digest-rating {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding-block-start: 4px;
  border-block-start: 1px solid var(--d-line-soft);
}
.d2-digest-rating-num { font-size: var(--fsz-h1); font-weight: 700; color: var(--d-ink); font-variant-numeric: tabular-nums; }
.d2-digest-rating-stars { color: var(--d-warn); font-size: var(--fsz-body); }
.d2-digest-rating-stars .d2-star { opacity: 0.25; }
.d2-digest-rating-stars .d2-star.is-on   { opacity: 1; }
.d2-digest-rating-stars .d2-star.is-half { opacity: 0.55; }
.d2-digest-rating-label { font-size: var(--fsz-caption); color: var(--d-ink-3); margin-inline-start: auto; }
/* ── DigestCheckins card ──
   Reframed around automation rate. The hero is the big % number —
   "how close to fully automated are we today?" — tinted green at
   (near) full automation, ink in the acceptable range, warn when
   the manual share starts to grow. A single-track meter underneath
   visualises the ratio with the manual share as a warn-tinted tail.
   An exception line surfaces the absolute manual count and the
   day's volume context.

   Old `.d2-checkin-bar / -seg` removed — the symmetric two-segment
   bar implied that manual and automated are equal peers, but they
   aren't: manual is the exception we want to flag. */
.d2-checkin-hero.is-ok      .d2-digest-hero-num { color: var(--d-ok,   oklch(0.55 0.13 150)); }
.d2-checkin-hero.is-warn    .d2-digest-hero-num { color: var(--d-warn, oklch(0.70 0.14  70)); }
.d2-checkin-hero.is-neutral .d2-digest-hero-num { color: var(--d-ink, var(--ink)); }
/* Single-track meter — flex with two basis-driven children. When
   manual is zero the second segment is omitted in JSX and the
   green segment fills the full track. */
/* Hit-zone wrapper — extends the meter's hover area from the visible
   6px bar to a comfortable ~20px so the cursor doesn't have to land
   on a hairline. The visible bar stays exactly where it was; only the
   invisible padding around it grew. */
.d2-checkin-meter-zone {
  padding-block: 7px;
  margin-block-start: 1px;
  margin-block-end: 1px;
  cursor: default;
}
.d2-checkin-meter {
  display: flex;
  block-size: 6px;
  inline-size: 100%;
  gap: 3px;
}
.d2-checkin-meter-auto,
.d2-checkin-meter-manual {
  block-size: 100%;
  border-radius: 999px;
  transition: flex-basis 240ms ease;
}
.d2-checkin-meter-auto   { background: var(--d-ok,   oklch(0.55 0.13 150)); }
.d2-checkin-meter-manual { background: var(--d-warn, oklch(0.70 0.14  70)); }
/* Breakdown popover — surfaces the absolute counts and percentages
   for each meter segment. Inner content only — outer chrome (bg,
   border, shadow, arrow) comes from the ChartTip primitive. */
.d2-checkin-pop {
  display: grid;
  gap: 8px;
  min-inline-size: 200px;
  padding: 2px 0;
}
.d2-checkin-pop-head {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tooltip-fg-muted);
}
.d2-checkin-pop-row {
  display: grid;
  grid-template-columns: 10px 1fr auto auto;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-label);
  color: var(--tooltip-fg);
}
.d2-checkin-pop-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 999px;
}
.d2-checkin-pop-dot.is-ok   { background: var(--d-ok,   oklch(0.55 0.13 150)); }
.d2-checkin-pop-dot.is-warn { background: var(--d-warn, oklch(0.70 0.14  70)); }
.d2-checkin-pop-label { color: var(--tooltip-fg-muted); }
.d2-checkin-pop-val   { font-weight: 600; color: var(--tooltip-fg); }
.d2-checkin-pop-pct   {
  font-weight: 600;
  color: var(--tooltip-fg-muted);
  min-inline-size: 36px;
  text-align: end;
}
.d2-checkin-pop-total {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-block-start: 6px;
  border-block-start: 1px solid var(--tooltip-border);
  font-size: var(--fsz-caption);
}
.d2-checkin-pop-total-label {
  color: var(--tooltip-fg-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.d2-checkin-pop-total-val { font-weight: 600; color: var(--tooltip-fg); }
/* Exception line — warn tone when manual > 0, ok tone when
   everything is automated. Single readable sentence pattern
   ("2 of 14 manual today") so the trailing total isn't a tiny
   muted afterthought. 13px / ink-2 to match dashboard row size. */
.d2-checkin-exception {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-body-sm);
  line-height: 1.35;
  color: var(--d-ink-2, var(--ink-2));
}
.d2-checkin-exception-icon {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}
.d2-checkin-exception.is-warn .d2-checkin-exception-icon { color: var(--d-warn, oklch(0.70 0.14 70)); }
.d2-checkin-exception.is-ok   .d2-checkin-exception-icon { color: var(--d-ok,   oklch(0.55 0.13 150)); }
.d2-checkin-exception-text {
  flex: 1;
  color: var(--d-ink-2, var(--ink-2));
}
.d2-checkin-exception-text b { font-weight: 700; }
.d2-checkin-exception.is-warn .d2-checkin-exception-text b { color: var(--d-warn, oklch(0.70 0.14 70)); }
.d2-checkin-exception.is-ok   .d2-checkin-exception-text b { color: var(--d-ok,   oklch(0.55 0.13 150)); }

.d2-yard-status { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
/* `.d2-yard-status-pill` MIGRATED to `.pill.is-ok` + `.pill-dot`.
   Class names kept as addressing hooks; no styling here. */
/* Status meta bumped 11→12px so it reads at the same weight as
   the status pill (12px) on the same row. */
.d2-yard-status-meta { font-size: var(--fsz-label); color: var(--d-ink-3); }
.d2-yard-bars {
  display: flex;
  gap: 1.5px;
  align-items: end;
  block-size: 28px;
}
.d2-yard-bar {
  flex: 1;
  background: var(--d-ok);
  block-size: 100%;
  border-radius: 1.5px;
  min-inline-size: 3px;
  cursor: default;
  outline: none;
  transition: filter 120ms ease, box-shadow 120ms ease;
}
.d2-yard-bar:hover,
.d2-yard-bar:focus-visible {
  /* Subtle highlight on the hovered bar so the user can tell
     which day's tooltip they're seeing — bars are thin (3px),
     so the box-shadow halo is more visible than a scale change. */
  filter: brightness(1.08);
  box-shadow: 0 0 0 1px var(--d-bg-card), 0 0 0 2px currentColor;
}
.d2-yard-bar.is-warn    { background: var(--d-warn); block-size: 60%; color: var(--d-warn); }
.d2-yard-bar.is-err     { background: var(--d-err);  block-size: 35%; color: var(--d-err);  }
.d2-yard-bar.is-down    { background: var(--d-err);  block-size: 35%; color: var(--d-err);  }
.d2-yard-bar.is-offline { background: var(--d-ink-4); block-size: 30%; color: var(--d-ink-4); }
.d2-yard-bar.is-ok      { color: var(--d-ok); }
/* Axis labels bumped 10→11px for readability — 10px was below
   the dashboard's general body floor. */
.d2-yard-axis { display: flex; justify-content: space-between; font-size: var(--fsz-caption); color: var(--d-ink-4); padding-block-start: 2px; }
/* Last-incident row: 11→13px body size (matches the table-typography
   rule's regular-cell size) and a generous margin above the divider
   so the row isn't crammed against the 30-day bars. */
.d2-yard-incident {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px;
  margin-block-start: 12px;
  padding-block-start: 12px;
  font-size: var(--fsz-body-sm);
  border-block-start: 1px solid var(--d-line-soft);
}

/* Hover popover anchored to a yard bar — same chrome as other
   dashboard tooltips. Portaled to <body>, position: fixed via JS-
   computed coords. The transform centers the card horizontally
   above the bar and lifts it 100% of its own height so the bar is
   visible. */
/* Yard-connectivity per-day bar tooltip — INNER content only.
   Outer chrome comes from the wrapping <ChartTip>. */
.d2-yard-pop {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-inline-size: 140px;
}
.d2-yard-pop-day {
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--tooltip-fg-muted);
}
.d2-yard-pop-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-label);
  color: var(--tooltip-fg);
}
.d2-yard-pop-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.d2-yard-pop-dot.is-ok      { background: var(--ok); }
.d2-yard-pop-dot.is-warn    { background: var(--warn); }
.d2-yard-pop-dot.is-err     { background: var(--err); }
.d2-yard-pop-dot.is-down    { background: var(--err); }
.d2-yard-pop-dot.is-offline { background: var(--tooltip-fg-muted); }
.d2-yard-pop-sub {
  font-size: var(--fsz-caption);
  color: var(--tooltip-fg-muted);
}

/* ── Main column sections ── */
.d2-main { display: flex; flex-direction: column; gap: 16px; }
.d2-section {
  /* Light mode keeps `--d-bg-card` (white) — operator preference was
     to leave light mode untouched. Dark-mode container lift via the
     [data-theme="dark"] override below. */
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: var(--r-lg);
  padding: 18px 22px;
  box-shadow: var(--d-sh-card);
  display: flex;
  flex-direction: column;
  gap: 14px;
}
[data-theme="dark"] .d2-section {
  /* In dark mode, drop to the page tier so content inside (LiveTest
     cards, tables, chart panels — all using --d-bg-card or
     --bg-raised) visibly elevates above the section's surface. Same
     elevation pattern as L1's .branch-deep above. */
  background: var(--d-bg-page);
}
.d2-section-head {
  /* Three-column grid: [title] [optional inline badge] [filler]
     [tools]. Title in row 1, meta in row 2 (when present), tools
     pinned to the right. The middle "auto" column reserves space
     for an optional `titleBadge` (e.g. the live-tests pill) that
     sits right next to the title without being styled as a heading. */
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  column-gap: 10px;
  row-gap: 2px;
  color: var(--d-ink);
}
/* Section title — small-caps gray label rather than a heading.
   The white card chrome (border + shadow) IS the section
   boundary; the label just names what's inside. Pattern matches
   Linear / Stripe / Vercel section labels. */
.d2-section-title {
  grid-column: 1;
  grid-row: 1;
  margin: 0;
  font-size: var(--fsz-label);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--d-ink-3);
  min-width: 0;
}
/* Inline badge — sits in column 2 on the same row as the title.
   Keeps its own typography (the pill's font/casing), unaffected
   by the title's uppercase + small-caps. */
.d2-section-title-badge {
  grid-column: 2;
  grid-row: 1;
  justify-self: start;
  display: inline-flex;
  align-items: center;
}
.d2-section-meta {
  grid-column: 1 / span 3;
  grid-row: 2;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3);
}
/* Spacer is a no-op in the new grid layout — title/meta/tools are
   placed by grid-column rules, no flex-fill needed. */
.d2-section-spacer { display: none; }
/* Anything else inside the section head (tools — view-all links,
   filter pills, etc.) lands in column 4 and centers vertically
   across both rows. */
.d2-section-head > *:not(.d2-section-title):not(.d2-section-title-badge):not(.d2-section-meta):not(.d2-section-spacer) {
  grid-column: 4;
  grid-row: 1 / -1;
  align-self: center;
}
/* At narrow widths the tools slot (e.g. .d2-live-tools — List/Cards
   toggle + "View All Live Tests" link) outgrows the 4th grid column
   and overlaps the title + badge. Drop tools onto their own row
   spanning the full width. Also let the title-row + badge collapse
   so they don't fight for horizontal space. */
@media (max-width: 720px) {
  .d2-section-head {
    grid-template-columns: auto 1fr;
    row-gap: 8px;
  }
  .d2-section-title { grid-column: 1; grid-row: 1; }
  .d2-section-title-badge { grid-column: 2; grid-row: 1; }
  .d2-section-spacer { display: none; }
  .d2-section-head > *:not(.d2-section-title):not(.d2-section-title-badge):not(.d2-section-meta):not(.d2-section-spacer) {
    grid-column: 1 / -1;
    grid-row: 2;
    justify-self: stretch;
  }
  /* When tools wrap to their own row, let the inner inline-flex
     children (segment toggle + link) wrap and space-between
     so they don't squeeze together. */
  .d2-live-tools { flex-wrap: wrap; justify-content: space-between; row-gap: 6px; }
}
.d2-section-link {
  font-size: var(--fsz-label);
  color: var(--brand-700);
  text-decoration: none;
  font-weight: 600;
  /* Unified inline-link treatment — color darken + soft brand bg
     tint on hover, never underline. */
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.d2-section-link:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.d2-section-link:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
.d2-section-body {}
.d2-pill-group { display: inline-flex; gap: 4px; }
.d2-pill {
  font-size: var(--fsz-caption);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  border: 1px solid var(--d-line);
  background: transparent;
  color: var(--d-ink-3);
  font-weight: 500;
  display: inline-flex;
  gap: 5px;
  align-items: center;
}
.d2-pill.is-on { background: var(--d-bg-page); color: var(--d-ink); border-color: var(--d-line); }
.d2-pill b { color: var(--d-ink); font-weight: 700; font-variant-numeric: tabular-nums; }

/* Live pill — pulsing red dot communicates "tests are running RIGHT
   NOW". The ::after ring expands and fades out, mimicking a
   broadcast-style "on air" indicator. Honors `prefers-reduced-motion`
   below. */
.d2-live-pill .d2-live-pulse {
  position: relative;
  inline-size: 7px;
  block-size: 7px;
  border-radius: 50%;
  background: var(--d-err);
  flex-shrink: 0;
}
.d2-live-pill .d2-live-pulse::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--d-err);
  animation: d2-live-pulse-ring 1.6s ease-out infinite;
}
@keyframes d2-live-pulse-ring {
  0%   { opacity: 0.55; transform: scale(1); }
  100% { opacity: 0;    transform: scale(2.6); }
}
@media (prefers-reduced-motion: reduce) {
  .d2-live-pill .d2-live-pulse::after { animation: none; }
}

.d2-empty { text-align: center; color: var(--d-ink-3); padding: 24px; font-size: var(--fsz-body); }

/* ── LiveTestsSection ── */
/* Card view: 2-up while each card can be ≥ 330px wide; below that the
   grid drops to 1-up. Threshold runs off the grid's own parent width
   (via a container query) so the break responds to actual available
   space, not viewport — works correctly inside narrow main columns,
   sidebars, etc. Math: 2 × 330 + 14 gap = 674px. List view stacks
   strips vertically. The `seg-toggle` + View-all link share the
   section's `tools` slot, wrapped by .d2-live-tools so they sit on
   the same row at a consistent gap. */
:has(> .d2-live-grid) { container-type: inline-size; }
/* L3 live-tests cards: 3-up when the section can hold 3 at the card's
   ~330px min (≥1018px), 2-up when tighter, 1-up below 674px. Capped at 3
   (no 4-up on ultrawide). Container-query scoped — yardlive untouched. */
.d2-live-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
@container (max-width: 1017px) { .d2-live-grid { grid-template-columns: 1fr 1fr; } }
@container (max-width: 673px) { .d2-live-grid { grid-template-columns: 1fr; } }
/* Compact view — multi-column strips. The strip (.lt-h1) has a 380px
   floor (rule below), so auto-fit packs as many ≥380px strips as the
   section can hold: ~2-up on L3's main column, 3+ on wider surfaces.
   Short rows × columns = far more live tests visible per screen. */
.d2-live-rows { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 8px; }
.d2-live-tools { display: inline-flex; align-items: center; gap: 12px; }

/* Minimum strip width — both dashboard list view (.d2-live-rows)
   and yardlive canvas list view (.ylc-grid--list) constrain
   their strip articles to at least 380px wide. Below that, the
   3-or-4 column layout (avatar + identity + actions + chevron)
   starts crushing the identity column to the point that the
   name marquee can't render properly. 380px is the empirically-
   safe floor that fits a single-line student name + sub-name +
   trailing-action cluster without overflow. */
.d2-live-rows .lt-h1,
.ylc-grid--list .lt-h1 {
  min-inline-size: 380px;
}

/* Dashboard-only: pin the assigned card's maneuvers row to the
   bottom of the card so it vertically aligns with the segment-bar
   row at the bottom of the adjacent ongoing LiveTestCard. The
   card itself stretches to fill its grid row (matches the
   tallest sibling — usually an ongoing card, which carries more
   content); the auto top-margin on the maneuvers row then
   consumes the spare vertical space pushing the row down. Result:
   both card types end with their "maneuvers" row at the same
   vertical position, giving the dashboard a consistent baseline.
   Scoped to `.d2-live-grid` so yardlive's canvas (where the
   assigned card has its own actions row pinning maneuvers from
   below) is unaffected. */
.d2-live-grid .ylc-assign-card {
  block-size: 100%;
}
.d2-live-grid .ylc-assign-card .ylc-assign-maneuvers {
  margin-block-start: auto;
  /* Breathing room above the pills (so they don't hug the facts
     row when content above is short) and below (so the last
     row doesn't hug the card's bottom border — yardlive's
     .ylc-assign-actions normally provides this bottom padding,
     but the dashboard skips that row). */
  padding-block-start: 12px;
  padding-block-end: 14px;
}

/* Read-only "Preferred language" value, trailing-edge of the
   langrow. Sits in the same slot yardlive's interactive
   `.seg-toggle.ylc-assign-langtoggle` lives in, but rendered as a
   plain value (not a control) on the read-only dashboard. Weight
   + ink color step up from the muted `.ylc-assign-langrow-key`
   so the row reads as label → value rather than two equal-weight
   items. Uses `--fsz-label` (same as the key) so the type scale
   stays consistent. */
.ylc-assign-langrow-value {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
  letter-spacing: 0.01em;
}

/* Slide-down accordion expand on the ongoing strip's segments
   row — shared between the dashboard (DashOngoingStrip) and
   yardlive's canvas (OngoingStripCompact). Both components now
   render the .ylc-strip-segments node UNCONDITIONALLY in the
   DOM and toggle visibility via `.is-expanded` on the parent
   article.

   Two-layer slide for a visible drawer feel:
     · Outer (.ylc-strip-segments): max-block-size 0 → 280px,
       opacity 0 → 1. The clipping container expands downward.
     · Inner (.lt-h1-segments): translateY(-10px) → 0. The
       actual content starts above its final position and slides
       DOWN into the clipping window as the window itself opens.
   Net effect: segments appear to slide out of the bottom of the
   strip rather than just fading-in-place.

   Curve: cubic-bezier(0.32, 0.72, 0, 1) — fast start, gentle
   decel. Same curve framer-motion uses for the row-level FLIP
   transition, so cross-row reorders and intra-row expand share
   the same motion vocabulary.

   Why CSS not JS:
     · Adding/removing the segments node from React state caused
       an instant DOM size change. Combined with framer-motion's
       layout-prop FLIP on the row wrapper, the whole row (avatar,
       name, etc.) appeared to "jump" / scale. Decoupling: framer
       handles cross-row reorders (via layout="position" on the
       wrappers — see LiveTestsSection in dashboard-views.jsx and
       LiveCanvas in yardlive.html), CSS handles in-row growth —
       the two no longer fight.

   `max-block-size` (not `block-size: auto`) — height: auto can't
   be transitioned. Capped at 280px which exceeds any realistic
   segments-row height (single row of bar+label, ~50–80px). */
/* Row-2 items on the ongoing strip get an explicit 8px top
   margin (because the strip's `row-gap` is set to 0 — see the
   `.lt-h1.ylc-strip-ongoing` rule further down in this file).
   This delivers the visible gap between row 1 (identity) and
   row 2 (status + buttons) without applying CSS Grid's `row-gap`
   to every pair of rows, which would otherwise add a phantom
   8px between row 2 and the trailing 0-height segments row when
   the strip is collapsed.
   Assigned strip gets the same treatment — see the matching
   row-gap: 0 + margin-block-start: 8px rules below. */
.lt-h1.ylc-strip-ongoing .ylc-strip-status-row,
.lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn,
.lt-h1.ylc-strip-ongoing .ylc-action--more,
.lt-h1.ylc-strip-assigned .ylc-strip-row1-info,
.lt-h1.ylc-strip-assigned .ylc-strip-expand-btn {
  margin-block-start: 8px;
}
/* Mirror the ongoing strip's row-gap: 0 fix on the assigned
   strip. Without this, a third row (the new pills accordion
   below) would inherit an 8px row-gap before it even when
   collapsed at 0px height, leaving phantom whitespace below
   the row 2 ready pill. The 8px gap above row 2 is now applied
   via the explicit margin rule directly above. */
.lt-h1.ylc-strip-assigned {
  row-gap: 0;
}

/* Drawer-slide rules — apply to BOTH the ongoing strip (inner
   content: .lt-h1-segments / bars) AND the assigned strip (inner
   content: .ylc-assign-maneuvers / pill chips). Same animation,
   same timing, same easing — the only difference is which inner
   element gets the translateY transform. */
.lt-h1.ylc-strip-ongoing .ylc-strip-segments,
.lt-h1.ylc-strip-assigned .ylc-strip-segments {
  max-block-size: 0;
  overflow: hidden;
  opacity: 0;
  padding-block-start: 0;
  margin-block-start: 0;
  transition:
    max-block-size 320ms cubic-bezier(0.32, 0.72, 0, 1),
    opacity 220ms ease-out,
    padding-block-start 320ms cubic-bezier(0.32, 0.72, 0, 1),
    margin-block-start 320ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* Assigned-strip-only: place the segments wrapper on row 3
   (avatar=row1, row1-info=row2 — same as ongoing). */
.lt-h1.ylc-strip-assigned .ylc-strip-segments {
  grid-column: 1 / -1;
  grid-row: 3;
}
.lt-h1.ylc-strip-ongoing .ylc-strip-segments .lt-h1-segments,
.lt-h1.ylc-strip-assigned .ylc-strip-segments .ylc-assign-maneuvers {
  /* Inner content sits 10px above its final position when
     collapsed and slides down into the clipping window. The
     transform is what makes the open/close read as a DRAWER
     SLIDE rather than a simple height reveal. Targets both the
     bar-row primitive (ongoing) and the pill-row primitive
     (assigned). */
  transform: translateY(-10px);
  transition: transform 320ms cubic-bezier(0.32, 0.72, 0, 1);
}
.lt-h1.ylc-strip-ongoing.is-expanded .ylc-strip-segments,
.lt-h1.ylc-strip-assigned.is-expanded .ylc-strip-segments {
  max-block-size: 280px;
  opacity: 1;
  padding-block-end: 8px;
  margin-block-start: 4px;
}
.lt-h1.ylc-strip-ongoing.is-expanded .ylc-strip-segments {
  /* Ongoing bars are thin (~8px tall); 18px above gives the
     bar row generous breathing space against the row-2 content
     (vehicle + timer + current pill). */
  padding-block-start: 18px;
}
.lt-h1.ylc-strip-assigned.is-expanded .ylc-strip-segments {
  /* Assigned pills are taller (~24px tall — the .ylc-assign-
     maneuver-pill primitive includes its own padding); they
     don't need as much space above to read as a separate row,
     and 18px started looking like dead whitespace between
     "READY TO START" and the pill set. Dialled down to 8px. */
  padding-block-start: 8px;
}
.lt-h1.ylc-strip-ongoing.is-expanded .ylc-strip-segments .lt-h1-segments,
.lt-h1.ylc-strip-assigned.is-expanded .ylc-strip-segments .ylc-assign-maneuvers {
  transform: translateY(0);
}
/* Reduced-motion: drop the transition; state change still works,
   but it snaps instead of sliding. */
@media (prefers-reduced-motion: reduce) {
  .lt-h1.ylc-strip-ongoing .ylc-strip-segments,
  .lt-h1.ylc-strip-assigned .ylc-strip-segments,
  .lt-h1.ylc-strip-ongoing .ylc-strip-segments .lt-h1-segments,
  .lt-h1.ylc-strip-assigned .ylc-strip-segments .ylc-assign-maneuvers {
    transition: none;
  }
}

/* ── SavedTestsSection ── */
.d2-saved-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 1279px) { .d2-saved-grid { grid-template-columns: 1fr; } }
.d2-saved-tile {
  background: var(--d-bg-card);
  border: 1px solid var(--d-line);
  border-radius: 12px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.d2-saved-tile-head { display: flex; align-items: center; gap: 12px; }
.d2-saved-tile-avatar {
  inline-size: 32px; block-size: 32px;
  border-radius: 50%;
  background: var(--d-line);
  color: var(--d-ink);
  display: grid; place-items: center;
  font-size: var(--fsz-caption); font-weight: 700;
}
.d2-saved-tile-name { font-size: var(--fsz-body); font-weight: 600; color: var(--d-ink); flex: 1; }
.d2-saved-tile-meta { font-size: var(--fsz-caption); color: var(--d-ink-3); }
.d2-saved-tile-pill {
  font-size: var(--fsz-caption); font-weight: 700; letter-spacing: 0.08em;
  padding: 3px 10px; border-radius: var(--r-pill);
}
.d2-saved-tile-pill.is-review   { color: var(--d-warn); background: var(--d-warn-soft); }
.d2-saved-tile-pill.is-disputed { color: var(--d-err);  background: var(--d-err-soft); }
.d2-saved-tile-meta-row { display: flex; gap: 16px; font-size: var(--fsz-caption); color: var(--d-ink-3); flex-wrap: wrap; }
.d2-saved-tile-meta-row b { color: var(--d-ink); font-weight: 600; }
.d2-saved-tile-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.d2-saved-tile-chip {
  font-size: var(--fsz-caption);
  padding: 3px 9px;
  border-radius: var(--r-pill);
  font-weight: 500;
}
.d2-saved-tile-chip.is-pass { color: var(--d-ok);  background: var(--d-ok-soft);  }
.d2-saved-tile-chip.is-fail { color: var(--d-err); background: var(--d-err-soft); }
.d2-saved-tile-foot {
  display: flex; align-items: center; gap: 12px;
  padding-block-start: 8px; border-block-start: 1px solid var(--d-line-soft);
}
.d2-saved-tile-note { flex: 1; font-size: var(--fsz-caption); color: var(--d-ink-3); }
.d2-saved-tile-open {
  appearance: none;
  background: var(--d-device);
  color: var(--d-device-fg);
  border: 0;
  font: inherit; font-size: var(--fsz-label); font-weight: 600;
  padding: 6px 14px;
  border-radius: 8px;
  cursor: pointer;
  transition: background 160ms ease;
}
.d2-saved-tile-open:hover { background: var(--d-indigo); color: white; }

/* ── ManeuverGrid ── */
.d2-man-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 16px;
}
@media (max-width: 1100px) { .d2-man-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px)  { .d2-man-grid { grid-template-columns: 1fr; } }
.d2-man-col {
  display: flex; flex-direction: column; gap: 8px;
  padding-inline-start: 12px;
  border-inline-start: 2px solid var(--d-line);
}
.d2-man-col.is-ok   { border-inline-start-color: var(--d-ok); }
.d2-man-col.is-warn { border-inline-start-color: var(--d-warn); }
.d2-man-col.is-err  { border-inline-start-color: var(--d-err); }
/* Clickable maneuver tile — quiet affordance (no card chrome change)
   plus a name underline + hover lift so the click target still
   communicates "this drills somewhere". */
.d2-man-col.is-clickable { cursor: pointer; transition: transform 160ms ease; }
.d2-man-col.is-clickable:hover { transform: translateY(-1px); }
/* No underline on hover — the parent card's translateY hover lift
   (above) is the affordance; underlining the name added a third
   visual cue that competed with that. */
.d2-man-col.is-clickable:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Maneuver-name header stays in primary ink regardless of pass-rate
   tone — the percentage number and the bar carry the tone signal,
   so coloring the name too was double-counting. No leading icon —
   the maneuver name carries the column's identity on its own
   (filled-illustration icons broke the line-icon system used
   elsewhere; per the design-review they were dropped in favour
   of a stronger name treatment). */
.d2-man-col-head { display: flex; align-items: center; gap: 8px; color: var(--d-ink); }
.d2-man-col-name { font-size: var(--fsz-h3); font-weight: 600; letter-spacing: -0.01em; }
.d2-man-col-num { display: flex; align-items: baseline; gap: 4px; }
.d2-man-col-num .tnum { font-size: var(--fsz-h1); font-weight: 700; color: var(--d-ink); letter-spacing: -0.01em; line-height: 1.1; font-variant-numeric: tabular-nums; }
.d2-man-col-pct { font-size: var(--fsz-body); color: var(--d-ink-3); }
.d2-man-col-bar {
  block-size: 5px;
  background: var(--d-err);
  border-radius: var(--r-pill);
  position: relative;
  overflow: hidden;
}
.d2-man-col-bar-pass { display: block; block-size: 100%; background: var(--d-ok); transition: inline-size 250ms ease; }
.d2-man-col.is-warn .d2-man-col-bar-pass { background: var(--d-warn); }
.d2-man-col.is-err  .d2-man-col-bar-pass { background: var(--d-err); }
.d2-man-col-counts {
  display: flex; align-items: baseline; gap: 4px;
  font-size: var(--fsz-caption); color: var(--d-ink-3);
}
.d2-man-col-counts .tnum { color: var(--d-ok); font-weight: 700; }
.d2-man-col-counts .is-fail { color: var(--d-err); }
.d2-man-col-spacer { flex: 1; }
.d2-man-col-genders { display: flex; flex-direction: column; gap: 4px; padding-block-start: 4px; }
.d2-man-col-gender { display: flex; align-items: center; gap: 6px; font-size: var(--fsz-caption); color: var(--d-ink-3); }
.d2-man-col-gender-letter {
  font-weight: 700;
  font-size: var(--fsz-chart-label);
  inline-size: 14px;
  text-align: center;
}
.d2-man-col-gender-letter.is-male   { color: var(--d-male); }
.d2-man-col-gender-letter.is-female { color: var(--d-female); }
.d2-man-col-gender-bar {
  flex: 1;
  block-size: 3px;
  background: var(--d-line-soft);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.d2-man-col-gender-bar > span { display: block; block-size: 100%; background: var(--d-male); transition: inline-size 250ms ease; }
.d2-man-col-gender-bar > span.is-female { background: var(--d-female); }
.d2-man-col-gender-pct { font-variant-numeric: tabular-nums; min-inline-size: 32px; text-align: end; color: var(--d-ink-2); font-weight: 600; }

/* ── FleetSection ── */
.d2-fleet-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px;
  padding: 12px 0;
  border-block-end: 1px solid var(--d-line-soft);
}
@media (max-width: 1100px) { .d2-fleet-strip { grid-template-columns: 1fr 1fr; } }
.d2-fleet-stat { display: flex; flex-direction: column; gap: 2px; }
.d2-fleet-stat-label { font-size: var(--fsz-caption); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--d-ink-3); }
.d2-fleet-stat-num { font-size: var(--fsz-h1); font-weight: 700; color: var(--d-ink); letter-spacing: -0.01em; line-height: 1.1; font-variant-numeric: tabular-nums; }
.d2-fleet-stat-unit { font-size: var(--fsz-label); font-weight: 500; color: var(--d-ink-3); }
.d2-fleet-stat-sub { font-size: var(--fsz-caption); color: var(--d-ink-3); }
.d2-fleet-stat-sub.is-ok { color: var(--d-ok); font-weight: 500; }
.d2-fleet-tbl {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px 12px;
  border: 1px solid var(--tbl-frame-border);
  border-radius: var(--tbl-frame-radius);
  background: var(--d-bg-card);
  overflow: hidden;
}
.d2-fleet-tbl-head, .d2-fleet-tbl-row {
  display: grid;
  grid-template-columns: 90px 1fr 80px 80px 80px;
  gap: 12px;
  align-items: center;
  font-size: var(--fsz-caption);
}
.d2-fleet-tbl-head {
  color: var(--tbl-header-fg);
  font-size: var(--fsz-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding-block-end: 6px;
  border-block-end: 1px solid var(--tbl-section-border);
}
.d2-fleet-tbl-row { padding-block: 6px; border-block-end: 1px solid var(--tbl-divider); }
.d2-fleet-tbl-row:nth-child(odd) { background: var(--tbl-row-stripe-bg); }
.d2-fleet-tbl-row:last-child { border-block-end: 0; }
.d2-fleet-veh-id {
  display: inline-flex; align-items: center; gap: 5px;
  font-weight: 600; color: var(--d-ink); font-size: var(--fsz-label);
}
.d2-fleet-veh-id.is-auto::before, .d2-fleet-veh-id.is-manual::before {
  content: ""; inline-size: 22px; block-size: 22px; border-radius: 5px;
  display: inline-block; flex-shrink: 0;
  background: var(--d-indigo);
}
.d2-fleet-veh-id.is-manual::before { background: var(--d-purple); }
.d2-fleet-tbl-util { display: block; }
.d2-fleet-util-bar {
  display: block;
  block-size: 8px;
  border-radius: var(--r-pill);
  background: var(--d-indigo);
  transition: inline-size 250ms ease;
}
.d2-fleet-util-bar.is-purple { background: var(--d-purple); }
.d2-fleet-util-bar.is-indigo { background: var(--d-indigo); }
.d2-fleet-util-bar.is-warn   { background: var(--d-warn); }
.d2-fleet-util-bar.is-err    { background: var(--d-err); }
/* Numeric data columns — regular weight per the table-typography
   rule (only the identity column .d2-fleet-veh-id is bold). */
.d2-fleet-tbl-row .num { text-align: end; font-variant-numeric: tabular-nums; color: var(--d-ink); font-weight: 400; }
.d2-fleet-tbl-row .num.is-err { color: var(--d-err); }

.d2-fleet-mini-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding-block-start: 12px; }
@media (max-width: 1100px) { .d2-fleet-mini-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px) { .d2-fleet-mini-grid { grid-template-columns: 1fr; } }
.d2-fleet-mini {
  border: 1px solid var(--d-line);
  border-radius: 10px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.d2-fleet-mini.is-ok   { border-color: color-mix(in oklab, var(--d-ok) 18%, var(--d-line)); }
.d2-fleet-mini.is-warn { border-color: color-mix(in oklab, var(--d-warn) 18%, var(--d-line)); }
.d2-fleet-mini.is-err  { border-color: color-mix(in oklab, var(--d-err) 22%, var(--d-line)); background: color-mix(in oklab, var(--d-err) 4%, var(--d-bg-card)); }
.d2-fleet-mini-head { display: flex; align-items: center; gap: 8px; }
.d2-fleet-mini-id { font-weight: 600; color: var(--d-ink); font-size: var(--fsz-label); flex: 1; display: inline-flex; align-items: center; gap: 5px; }
.d2-fleet-mini-id.is-auto::before, .d2-fleet-mini-id.is-manual::before {
  content: ""; inline-size: 18px; block-size: 18px; border-radius: 4px;
  display: inline-block; flex-shrink: 0;
  background: var(--d-indigo);
}
.d2-fleet-mini-id.is-manual::before { background: var(--d-purple); }
.d2-fleet-mini-pill { font-size: var(--fsz-caption); font-weight: 700; letter-spacing: 0.08em; padding: 2px 8px; border-radius: var(--r-pill); }
.d2-fleet-mini-pill.is-ok   { color: var(--d-ok);   background: var(--d-ok-soft); }
.d2-fleet-mini-pill.is-warn { color: var(--d-warn); background: var(--d-warn-soft); }
.d2-fleet-mini-pill.is-err  { color: var(--d-err);  background: var(--d-err-soft); }
.d2-fleet-mini-icon { display: grid; place-items: center; padding: 8px 0; color: var(--d-ink-3); }
.d2-fleet-mini.is-err .d2-fleet-mini-icon { color: var(--d-err); }
.d2-fleet-mini-sub {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 12px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-2);
}
.d2-fleet-mini-sub-cell { display: inline-flex; align-items: center; gap: 5px; }
.d2-fleet-mini-sub-dot { inline-size: 6px; block-size: 6px; border-radius: 50%; background: var(--d-ok); }
.d2-fleet-mini-sub-cell.is-ok  .d2-fleet-mini-sub-dot { background: var(--d-ok); }
.d2-fleet-mini-sub-cell.is-err .d2-fleet-mini-sub-dot { background: var(--d-err); }
.d2-fleet-mini-sub-cell.is-err { color: var(--d-err); }
.d2-fleet-mini-foot { display: flex; justify-content: space-between; gap: 8px; font-size: var(--fsz-caption); color: var(--d-ink-3); padding-block-start: 4px; border-block-start: 1px solid var(--d-line-soft); }
.d2-fleet-mini-foot b { color: var(--d-ink); font-weight: 700; }

/* ── ExaminersTable ── */
.d2-exam-tbl {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0 12px;
  border: 1px solid var(--tbl-frame-border);
  border-radius: var(--tbl-frame-radius);
  background: var(--d-bg-card);
  overflow: hidden;
}
.d2-exam-tbl-avatar {
  inline-size: 30px; block-size: 30px;
  border-radius: 50%;
  display: grid; place-items: center;
  color: white; font-weight: 700; font-size: var(--fsz-caption);
  flex-shrink: 0;
}
/* Photo variant of the examiner avatar — same size as the
   initials circle, but renders the synthetic photo from
   window.photoFor. Click opens the ProfilePopover. */
.d2-exam-tbl-photo {
  inline-size: 30px; block-size: 30px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  cursor: pointer;
  transition: box-shadow var(--t-fast);
  background: var(--d-line-soft);
}
/* Avatar hover ring — solid brand-500 at 2px, matching the canonical
   test-canvas / DataTable / live-test-card hover treatment. The
   earlier 40%-transparent indigo recipe + scale animation was a one-
   off; unified here so every avatar across the system reads the
   same. Avatar is the only popover trigger; the name (above) stays
   static. */
.d2-exam-tbl-photo:hover {
  box-shadow: 0 0 0 2px var(--brand-500);
}
/* Sub-line under the examiner name — the OPPOSITE-language name.
   Mirrors the saved-tests `.examiner-name-sub` pattern: the span
   is dir="rtl" for Arabic text glyphs but text-align is forced to
   the page's reading direction (left in LTR / right in RTL) so
   the line starts from the cell's leading edge regardless of the
   text's intrinsic direction. */
.d2-exam-tbl-name-sub {
  display: block;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3, var(--ink-3));
  font-weight: 400;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-inline-size: 100%;
  font-family: var(--font-ar, inherit);
}
[dir="ltr"] .d2-exam-tbl-name-sub { text-align: left; }
[dir="rtl"] .d2-exam-tbl-name-sub { text-align: right; font-family: inherit; }

/* Examiner-name typography — was a button styled as a link; now
   a plain `<span>` per the system rule ("avatar is the only
   popover trigger; name stays informational text"). Class name
   retains "-btn" suffix for historical continuity, but the element
   is no longer interactive: no cursor change, no hover state.
   font-weight 600 + inherited typography keep it visually
   consistent with the previous button rendering. */
.d2-exam-tbl-name-btn {
  font-weight: 600;
  color: inherit;
}
.d2-exam-tbl-avatar.is-tone-0 { background: var(--d-purple); }
.d2-exam-tbl-avatar.is-tone-1 { background: var(--d-indigo); }
.d2-exam-tbl-avatar.is-tone-2 { background: oklch(0.62 0.18 158); }
.d2-exam-tbl-avatar.is-tone-3 { background: oklch(0.55 0.22 350); }
.d2-exam-tbl-avatar.is-tone-4 { background: oklch(0.62 0.16 50);  }
.d2-exam-tbl-avatar.is-tone-5 { background: oklch(0.62 0.16 195); }
.d2-exam-tbl-row .num { text-align: end; font-variant-numeric: tabular-nums; }
.d2-exam-tbl-prbar {
  display: inline-block;
  inline-size: 60px;
  block-size: 5px;
  background: var(--d-line-soft);
  border-radius: var(--r-pill);
  overflow: hidden;
  vertical-align: middle;
  margin-inline-end: 8px;
}
.d2-exam-tbl-prbar > span { display: block; block-size: 100%; transition: inline-size 250ms ease; }
.d2-exam-tbl-prbar.is-ok   > span { background: var(--d-ok); }
.d2-exam-tbl-prbar.is-warn > span { background: var(--d-warn); }
.d2-exam-tbl-prbar.is-err  > span { background: var(--d-err); }
.d2-exam-tbl-prnum { font-variant-numeric: tabular-nums; font-weight: 600; }
.d2-exam-tbl-prnum.is-ok   { color: var(--d-ok); }
.d2-exam-tbl-prnum.is-warn { color: var(--d-warn); }
.d2-exam-tbl-prnum.is-err  { color: var(--d-err); }
.d2-exam-tbl-histo {
  display: flex;
  align-items: end;
  gap: 2px;
  block-size: 26px;
  position: relative;
}
.d2-exam-tbl-histo-bar {
  inline-size: 6px;
  block-size: 50%;
  background: var(--d-purple);
  opacity: 0.35;
  border-radius: 1px;
}
.d2-exam-tbl-histo-bar.is-active { opacity: 1; background: var(--d-ok); }
.d2-exam-tbl-histo-label { font-size: var(--fsz-caption); color: var(--d-ink-3); margin-inline-start: 8px; }

/* ── Examiner utilization bars (replaces .d2-exam-tbl-histo) ──
   Twelve narrow bars per row, one per shift hour (8am–8pm). Bar
   height = utilization % for that hour; tone classes (.is-high /
   .is-mid / .is-low) colour by intensity so a busy day reads as
   green pattern, an idle day reads as muted. */
.exam-util-wrap {
  display: inline-flex;
  align-items: end;
  cursor: default;
  outline: none;
  /* Same visual envelope as the previous histogram cell so the
     row height stays consistent. */
  block-size: 26px;
}
.exam-util-wrap:focus-visible {
  outline: 2px solid var(--d-indigo, var(--brand-500));
  outline-offset: 2px;
  border-radius: 3px;
}
.exam-util-bars {
  display: flex;
  align-items: end;
  gap: 2px;
  block-size: 100%;
  inline-size: 100%;
}
.exam-util-bar {
  /* 12 bars share the cell width; use flex:1 so they fill evenly
     regardless of column width. */
  flex: 1 1 0;
  min-inline-size: 4px;
  border-radius: 2px;
  transition: filter 120ms ease, background 120ms ease;
}
/* Three tone bands. Background-as-rgb soft so an idle (low) bar
   still shows a faint rail rather than disappearing. */
.exam-util-bar.is-high {
  background: var(--d-ok, oklch(0.55 0.13 150));
}
.exam-util-bar.is-mid {
  background: var(--d-warn, oklch(0.70 0.14 70));
}
.exam-util-bar.is-low {
  background: color-mix(in oklab, var(--d-ink, var(--ink)) 15%, transparent);
}
/* Hover/active emphasis — slight brightening + darker outline. */
.exam-util-bar.is-hov {
  filter: brightness(1.08);
  outline: 1px solid color-mix(in oklab, var(--d-ink, var(--ink)) 30%, transparent);
  outline-offset: 1px;
}

/* Hover-card variant for the util popover — uses the existing
   rate-cell-pop chrome so the visual matches the pass-rate cell's
   hover card. */
.rate-cell-pop.is-util .rate-cell-pop-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
  align-items: center;
  font-size: var(--fsz-caption);
}
.rate-cell-pop.is-util .rate-cell-pop-label {
  color: var(--tooltip-fg-muted);
}

/* `.info-tooltip-pop` — MIGRATED. InfoTooltipIcon now renders its
   text directly into a <ChartTip> portal (unified anchored tooltip
   primitive). The class no longer appears in any JSX; selector kept
   so historic third-party references don't break, but emits nothing. */

/* Column-header label paired with a small info icon. */
.th-with-info {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.th-info-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 14px;
  block-size: 14px;
  border-radius: 50%;
  color: var(--d-ink-3, var(--ink-3));
  outline: none;
  transition: color 120ms ease, background 120ms ease;
}
.th-info-icon:hover,
.th-info-icon:focus-visible {
  color: var(--d-ink, var(--ink));
  background: color-mix(in oklab, var(--d-ink, var(--ink)) 8%, transparent);
}
.th-info-icon:focus-visible {
  outline: 2px solid var(--d-indigo, var(--brand-500));
  outline-offset: 1px;
}

/* ── Examiner override-status cell ──
   Shows total count (large) plus a stacked composition bar
   underneath splitting approved / rejected / pending. Same
   visual chrome as GenderSplitBar, just with three segments.
   Hover surfaces per-status counts + percentages. */
.exam-ovr-empty {
  color: var(--d-ink-4);
  font-weight: 600;
}
.exam-ovr-wrap {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  /* Pinned to 110px (no flex) so the Overrides bar matches the
     Pass-Rate bar (.cell-pr wrap also pinned to 110) — and both
     match the Top-10 Fail-Rate column. Without an upper bound the
     auto-layout was giving Overrides 191px while Pass-Rate got
     150px, which read as the two metrics having different scales.
     Symmetry across the three cells:
       Examiners · Pass Rate    → 110px bar
       Examiners · Overrides    → 110px bar
       Top 10    · Fail Rate    → 110px bar
     all sharing number-on-top + 6px bar + 3px gap. */
  gap: 3px;
  cursor: default;
  outline: none;
  inline-size: 110px;
}
.exam-ovr-wrap:focus-visible {
  outline: 2px solid var(--d-indigo, var(--brand-500));
  outline-offset: 2px;
  border-radius: 3px;
}
.exam-ovr-count {
  /* Same table-typography rule: numeric cell, regular weight,
     13px. The bar is the visual; this is just the count above it.
     Only the first (identity) column on a table row is bold. */
  font-weight: 400;
  color: var(--d-ink);
  font-size: var(--fsz-body);
  line-height: 1;
}
.exam-ovr-bar {
  /* Bar height matches .rate-cell-bar (6px) so the Pass Rate and
     Overrides cells in the Examiners table share the same visual
     weight — and both line up with the Top-10 Fail Rate bar
     (also 6px). Previously 4px, which read as a thinner / less
     prominent indicator next to its 6px peer. */
  display: flex;
  block-size: 6px;
  inline-size: 100%;
  gap: 2px;
}
.exam-ovr-seg {
  block-size: 100%;
  border-radius: 999px;
  flex-shrink: 0;
}
.exam-ovr-seg.is-approved { background: var(--d-ok,   oklch(0.55 0.13 150)); }
.exam-ovr-seg.is-rejected { background: var(--d-err,  oklch(0.58 0.18  25)); }
.exam-ovr-seg.is-pending  { background: var(--d-warn, oklch(0.70 0.14  70)); }

/* Hover popover variant — uses rate-cell-pop chrome with a
   four-column row (dot · label · count · %). */
.rate-cell-pop.is-ovr .exam-ovr-pop-row {
  display: grid;
  grid-template-columns: 8px 1fr auto auto;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-caption);
}
.exam-ovr-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.exam-ovr-dot.is-approved { background: var(--d-ok,   oklch(0.55 0.13 150)); }
.exam-ovr-dot.is-rejected { background: var(--d-err,  oklch(0.58 0.18  25)); }
.exam-ovr-dot.is-pending  { background: var(--d-warn, oklch(0.70 0.14  70)); }
.rate-cell-pop.is-ovr .rate-cell-pop-pct {
  color: var(--tooltip-fg-muted);
}
.d2-exam-tbl-override-pill {
  display: inline-block;
  font-size: var(--fsz-caption);
  font-weight: 700;
  padding: 2px 9px;
  border-radius: var(--r-pill);
}
.d2-exam-tbl-override-pill.is-warn { color: var(--d-warn); background: var(--d-warn-soft); }

/* ── TopTenTable + grid ──
   The two TopTenTables (yard / road) share the .data-table.rpt-table-
   params chrome with the Reports module's Top-10 panel — same rank /
   parameter+sub / severity / count / % structure. They sit side-by-
   side at desktop and stack to one column at narrow viewports. */
.d2-toptens { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 16px; }

/* Dashboard hosts the Top-10 table inside a Section panel rather
   than the report's framed .rpt-table-wrap. .top10-shared-wrap
   exists so a couple of rules from the .rpt-table-wrap context can
   re-apply for this nested usage (right-aligned numeric headers,
   hover row tint) without the outer frame chrome. */
.top10-shared-wrap .data-table tr:hover { background: var(--tbl-row-hover-bg); }
.top10-shared-wrap .data-table th.num,
.top10-shared-wrap .data-table td.num { text-align: end; font-variant-numeric: tabular-nums; }
.top10-shared-wrap .data-table th.num .th-btn { justify-content: flex-end; }
/* The base .branch-cmp-tbl-wrap rule sets `overflow-x: auto` to allow
   horizontal scroll for the wide branch-comparison tables. That
   forces overflow-y to `auto` too (per CSS spec) and turns the wrap
   into a scroll container — which makes sticky thead position
   relative to the WRAP instead of the viewport, breaking the
   column-header stickiness completely. Our 3-col Top-10 table is
   narrow enough that it doesn't need horizontal scroll, so we
   restore overflow:visible here. */
.top10-shared-wrap { overflow: visible; }
/* At ≤1100px the parent thead is already `position: static` (see
   page.css `@media (max-width: 1100px) { .data-table thead th { position: static; } }`),
   so we don't need to preserve sticky positioning here. Re-enable
   horizontal scroll on the wrap at narrow widths so the table
   doesn't burst the viewport when the parameter column's long
   text squeezes the remaining columns off-screen. */
@media (max-width: 1100px) {
  .top10-shared-wrap { overflow-x: auto; }
}
/* Unify Top-10 typography with the rest of the dashboard tables
   (.branch-cmp-tbl uses 13px / 11px sub). Top-10 inherits .data-
   table's 12.5px base — bump it here so the parameter name reads
   at the same size as examiner / branch names elsewhere. */
.top10-shared-tbl,
.top10-shared-tbl td,
.top10-shared-tbl .mono { font-size: var(--fsz-body); }
/* Clickable row affordance — quiet hover background tint plus focus
   ring for keyboard users. Sets cursor only on rows that opted in
   via the .top10-row-clickable class. */
.top10-shared-tbl tr.top10-row-clickable { cursor: pointer; }
.top10-shared-tbl tr.top10-row-clickable:hover td {
  background: color-mix(in oklab, var(--brand-500) 6%, transparent);
}
.top10-shared-tbl tr.top10-row-clickable:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: -2px;
}
.top10-shared-tbl .cell-strong { font-weight: 600; color: var(--d-ink, var(--ink)); }
.top10-shared-tbl .top10-sub { font-size: var(--fsz-caption); }
/* Sticky-header offset override for the dashboard context. The
   global `.data-table thead th { top: 104px }` is calibrated for
   Reports (TopNav 64 + compact strip 40). On the dashboard the
   scroll container is .d2-main (not the body), so sticky pins
   relative to .d2-main — `top: 0` matches what every other
   dashboard table (.branch-cmp-tbl thead) does. */
.top10-shared-wrap .data-table thead th { top: 0; }

/* Failure-rate column — number stacks above the bar so the
   column doesn't need horizontal room for the percentage. Width
   sized to fit a 110px bar plus the 16px right padding so the bar
   matches the Examiners table's Pass Rate (.cell-pr → 110px) and
   Overrides (.exam-ovr-wrap → 110px). Single shared bar dimension
   across the system: 110px × 6px on all three. */
.data-table.top10-shared-tbl colgroup .col-failrate { width: 130px; }
/* Maneuver / Category column — promoted to its own column now that the
   table runs full-width; the parameter no longer carries it in a
   sub-line. Mirrors the Reports Test-Analysis Top-10 layout. The
   adjacent Severity column reuses the shared .col-sev width (100px). */
.data-table.top10-shared-tbl colgroup .col-maneuver { width: 168px; }
.top10-shared-tbl .top10-cell-maneuver {
  color: var(--d-ink-2, var(--ink-2));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.top10-shared-tbl .top10-cell-sev { white-space: nowrap; }
/* At ≤720px force a natural table min-width so the parameter
   column stops aggressive ellipsis-truncation. Combined with
   `overflow-x: auto` on `.top10-shared-wrap` (above), this matches
   the examiners-table pattern: section stays contained inside
   the viewport, table scrolls horizontally INSIDE the section card
   so the user can read the full parameter name by swiping. */
@media (max-width: 720px) {
  .top10-shared-tbl { min-width: 680px; }
}
/* Override tooltip-cell overflow for the failure-rate cell so the
   rate-cell-pop hover card escapes the table-cell clipping context. */
.data-table.top10-shared-tbl td:has(.rate-cell.is-fail-rate) { overflow: visible; }
.rate-cell.is-fail-rate { min-inline-size: 0; inline-size: 110px; }
/* Right padding so the bar doesn't hug the table's right edge. */
.data-table.top10-shared-tbl thead th:last-child .th-btn,
.data-table.top10-shared-tbl tbody td:last-child {
  padding-inline-end: 16px;
}
/* Fail share full-red regardless of hover context (the failure
   rate IS the metric here, not a peer indicator). */
.rate-cell.is-fail-rate .rate-cell-seg.is-fail {
  background: var(--fail);
}
/* Faded pass-color segment used by FailRateCell — represents the
   non-failure remainder so the bar reads as composition rather
   than just an empty rail. */
.rate-cell-seg.is-pass-faded {
  display: block;
  block-size: 6px;
  border-radius: var(--r-pill);
  background: var(--pass-border);
}
/* Parameter cell sub-line: group · severity. Sev tone tints just the
   severity word so the row reads as: "Slope · Minor" with Minor in
   warn / Major in err. Kept on a single line and (if the dot+sev
   end up overflowing in a narrow column) clipped with the same
   ellipsis the rest of the cell uses. */
.top10-shared-tbl .top10-sub {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--d-ink-3, var(--ink-3));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.top10-shared-tbl .top10-sub-group {
  color: var(--d-ink-3, var(--ink-3));
}
.top10-shared-tbl .top10-sub-sep {
  color: var(--d-ink-4, var(--ink-4));
  /* Align the middle-dot vertically so it doesn't sit too low. */
  line-height: 1;
}
.top10-shared-tbl .top10-sub-sev {
  font-weight: 600;
}
.top10-shared-tbl .top10-sub-sev.is-major   { color: var(--d-err,  oklch(0.58 0.18 25)); }
.top10-shared-tbl .top10-sub-sev.is-minor   { color: var(--d-warn, oklch(0.70 0.14 70)); }
/* Neutral tone — informational only, not an error or warning.
   Reads as a muted blue / slate so it sits below Minor in
   visual weight. */
.top10-shared-tbl .top10-sub-sev.is-neutral { color: var(--d-ink-3, var(--ink-3)); }

/* Fail-rate bar + value — same idea as the old .d2-top-tbl-bar. */
.top10-bar-row {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  vertical-align: middle;
}
.top10-bar {
  display: inline-block;
  inline-size: 60px;
  block-size: 5px;
  background: var(--d-line-soft);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.top10-bar > span { display: block; block-size: 100%; transition: inline-size 250ms ease; }
.top10-bar.is-err     > span { background: var(--d-err); }
.top10-bar.is-warn    > span { background: var(--d-warn); }
.top10-bar.is-neutral > span { background: var(--d-ink-3); }
.top10-pct { font-variant-numeric: tabular-nums; font-weight: 700; }
.top10-pct.is-err     { color: var(--d-err); }
.top10-pct.is-warn    { color: var(--d-warn); }
.top10-pct.is-neutral { color: var(--d-ink-2); }

/* ════════════════════════════════════════════════════════════════
   MAPVIEW polish — rank emphasis on top performers
   Same gold/silver/bronze treatment as InstituteCard so List ↔ Map
   reads as the same visual language. Rank-1 gets a pulsing halo to
   communicate "live #1 right now" without animation noise on the
   rest of the map.
   ════════════════════════════════════════════════════════════════ */
.map-bar-pulse {
  fill: oklch(0.62 0.16 85);
  fill-opacity: 0.18;
  stroke: oklch(0.62 0.16 85);
  stroke-width: 1.2;
  stroke-opacity: 0.6;
  transform-origin: center;
  animation: map-bar-pulse-ring 1.8s ease-out infinite;
  pointer-events: none;
}
@keyframes map-bar-pulse-ring {
  0%   { transform: scale(0.7); opacity: 1; }
  100% { transform: scale(2.2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .map-bar-pulse { animation: none; opacity: 0.6; }
  /* New T3 components (AlertToast + KPI block) — disable their
     entrance animations + transitions for users that opted out of
     motion. The state changes still happen instantly so no info is
     lost; only the easing animation is skipped. */
  .alert-toast { animation: none; }
  .alert-toast-progress { transition: none; }
  .bx-kpi-segbar-seg { transition: none; }
  .bx-kpi-progress-fill { transition: none; }
  .bx-trend-axis-tick, .bx-trend-svg path, .bx-kpi-spark-lg path { transition: none; }
}
.map-bar-rank {
  fill: var(--bg-raised);
  stroke: var(--line);
  stroke-width: 0.8;
}
.map-bar-rank.is-rank-1 {
  fill: oklch(0.92 0.16 85);
  stroke: oklch(0.62 0.16 85);
}
.map-bar-rank.is-rank-2 {
  fill: oklch(0.95 0.02 280);
  stroke: oklch(0.65 0.04 280);
}
.map-bar-rank.is-rank-3 {
  fill: oklch(0.92 0.06 60);
  stroke: oklch(0.55 0.10 50);
}
.map-bar-rank-num {
  font-size: var(--fsz-chart-label);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  fill: var(--ink-2);
  pointer-events: none;
}
.map-bar-rank-num.is-rank-1 { fill: oklch(0.32 0.12 60); }
.map-bar-rank-num.is-rank-2 { fill: oklch(0.30 0.04 280); }
.map-bar-rank-num.is-rank-3 { fill: oklch(0.30 0.08 30); }

/* ════════════════════════════════════════════════════════════════
   STACKED BAR — hover state (segment) + tooltip pop
   ════════════════════════════════════════════════════════════════ */
.dash-stack-seg.is-active {
  filter: brightness(1.15);
  outline: 1.5px solid var(--bg-raised);
  outline-offset: -2px;
}

/* ════════════════════════════════════════════════════════════════
   BULLETCHART — Tier-1 InstituteCard footer bars
   Each row shows actual vs cohort target with a tone-coloured fill.
   Tone: ok = above target, warn = within 15%, err = below target.
   `inverted` flips meaning for "lower is better" metrics (Pending,
   Failed) so the bar still reads green-when-good.
   ════════════════════════════════════════════════════════════════ */
.dash-bullet {
  display: block;
  position: relative;
  margin-block-start: 4px;
}
.dash-bullet-track {
  display: block;
  block-size: 100%;
  background: var(--bg-muted);
  border-radius: var(--r-pill);
  position: relative;
  overflow: hidden;
}
.dash-bullet-fill {
  display: block;
  block-size: 100%;
  border-radius: inherit;
  transition: inline-size var(--t-med);
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--brand-500) 75%, transparent),
    var(--brand-500)
  );
}
.dash-bullet.is-ok   .dash-bullet-fill { background: linear-gradient(90deg, color-mix(in oklab, var(--ok) 70%, transparent), var(--ok)); }
.dash-bullet.is-warn .dash-bullet-fill { background: linear-gradient(90deg, color-mix(in oklab, var(--warn) 70%, transparent), var(--warn)); }
.dash-bullet.is-err  .dash-bullet-fill { background: linear-gradient(90deg, color-mix(in oklab, var(--err) 70%, transparent), var(--err)); }
.dash-bullet-target {
  position: absolute;
  inset-block: -2px;
  inline-size: 2px;
  background: var(--ink);
  border-radius: 1px;
  transform: translateX(-50%);
  pointer-events: none;
}
[dir="rtl"] .dash-bullet-target { transform: translateX(50%); }
.dash-bullet-pro {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px 0;
}
.dash-bullet-pro-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fsz-label);
}
.dash-bullet-pro-label { color: var(--ink-2); }
.dash-bullet-pro-sub { color: var(--ink); font-weight: 700; }
.dash-bullet-pro-svg { inline-size: 100%; block-size: 22px; display: block; }
.dash-bullet-pro-band.is-err  { fill: var(--err); fill-opacity: 0.20; }
.dash-bullet-pro-band.is-warn { fill: var(--warn); fill-opacity: 0.20; }
.dash-bullet-pro-band.is-ok   { fill: var(--ok); fill-opacity: 0.20; }
.dash-bullet-pro-fill { fill: var(--brand-600); }
.dash-bullet-pro.is-ok   .dash-bullet-pro-fill { fill: var(--ok); }
.dash-bullet-pro.is-warn .dash-bullet-pro-fill { fill: var(--warn); }
.dash-bullet-pro.is-err  .dash-bullet-pro-fill { fill: var(--err); }
.dash-bullet-pro-target {
  stroke: var(--ink);
  stroke-width: 2.5;
  stroke-linecap: round;
}
.dash-bullet-pro-vals {
  font-size: var(--fsz-caption);
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
.dash-bullet-pro-target-label { color: var(--ink-3); }

/* ════════════════════════════════════════════════════════════════
   RADAR — multi-axis spider for ManeuversWidget radar mode
   Spokes + concentric rings for context; series polygon over top
   with low-alpha fill + crisp stroke.
   ════════════════════════════════════════════════════════════════ */
.man-radar-wrap { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 8px 0; }
.man-radar-legend { display: flex; gap: 12px; font-size: var(--fsz-label); color: var(--ink-2); }
.dash-radar-wrap { width: 100%; max-width: 320px; }
.dash-radar { inline-size: 100%; block-size: auto; display: block; }
.dash-radar-ring  { fill: none; stroke: var(--line); stroke-width: 0.8; stroke-dasharray: 2 3; }
.dash-radar-spoke { stroke: var(--line); stroke-width: 0.8; }
.dash-radar-axis  { font-size: var(--fsz-caption); fill: var(--ink-3); font-weight: 600; letter-spacing: 0.02em; }

/* ════════════════════════════════════════════════════════════════
   UPTIME CALENDAR — Tier-2 YardStatusCard 30-day strip
   GitHub-style cells with state-tinted intensity + hover tooltip.
   Right-most cell = today (matches the legacy `dom-uptime-bars`).
   ════════════════════════════════════════════════════════════════ */
.dash-uptime-cal {
  display: flex;
  gap: 2px;
  align-items: stretch;
  inline-size: 100%;
}
.dash-uptime-cell {
  flex: 1;
  block-size: 100%;
  border-radius: 2px;
  background: color-mix(in oklab, var(--ok) 28%, var(--bg-muted));
  transition: filter var(--t-fast), transform var(--t-fast);
  cursor: pointer;
  min-inline-size: 4px;
}
.dash-uptime-cell.is-ok      { background: var(--ok); }
.dash-uptime-cell.is-warn    { background: var(--warn); }
.dash-uptime-cell.is-err     { background: var(--err); }
.dash-uptime-cell.is-down    { background: var(--err); }
.dash-uptime-cell.is-offline { background: var(--ink-4); }
.dash-uptime-cell.is-active {
  filter: brightness(1.2);
  outline: 1.5px solid var(--bg-raised);
  outline-offset: -1.5px;
}

/* ════════════════════════════════════════════════════════════════
   HISTOGRAM gradient — vertical bar fade matching sparkline area
   ════════════════════════════════════════════════════════════════ */
.histo.is-grad .histo-bar {
  background: linear-gradient(180deg,
    var(--histo-accent, var(--brand-600)),
    color-mix(in oklab, var(--histo-accent, var(--brand-600)) 35%, transparent)
  );
}
.histo:not(.is-grad) .histo-bar {
  background: var(--histo-accent, var(--brand-600));
}

/* ════════════════════════════════════════════════════════════════
   TOP-10 severity legend + texture patterns
   Patterns supplement colour (WCAG color-not-only) so severity reads
   even with red-green colour deficiency. Diagonal stripes = critical,
   subtle dots = major, plain fill = minor.
   ════════════════════════════════════════════════════════════════ */
.top10-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 0 0 8px;
  font-size: var(--fsz-caption);
  color: var(--ink-2);
}
.top10-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.top10-legend-sw {
  inline-size: 14px;
  block-size: 8px;
  border-radius: 2px;
}
.top10-legend-sw.is-critical {
  background:
    repeating-linear-gradient(
      135deg,
      var(--err) 0 3px,
      color-mix(in oklab, var(--err) 55%, white) 3px 6px
    );
}
.top10-legend-sw.is-major {
  background:
    radial-gradient(circle at 25% 50%, white 1px, transparent 1.5px) 0 0/4px 4px,
    var(--err);
}
.top10-legend-sw.is-minor { background: var(--warn); }

/* Apply same patterns to the actual fill bars so legend ↔ bar match. */
.top10-fill.is-critical {
  background:
    repeating-linear-gradient(
      135deg,
      var(--err) 0 4px,
      color-mix(in oklab, var(--err) 55%, white) 4px 8px
    );
}
.top10-fill.is-major {
  background:
    radial-gradient(circle at 25% 50%, white 1px, transparent 1.5px) 0 0/5px 5px,
    var(--err);
}
.top10-fill.is-minor { background: var(--warn); }

/* ═══════════════════════════════════════════════════════════════
   HomePage (home.html) — one outer whiteframe.
   The hero region sits on top of the body region inside the same
   card, separated by a hairline. Sections inside the body use
   lightweight subsection chrome (header + body, no per-section
   card frame).
   ═══════════════════════════════════════════════════════════════ */
/* Homepage page background — slightly sunken so the white outer
   card stands out as a clearly-defined container. Scoped via
   :has() so it doesn't pollute other pages. */
body:has(.hp-shell) {
  background: var(--bg, var(--bg-muted));
}
/* `.main--home` mirrors `.main--dash` padding so the homepage
   content lines up horizontally with the dashboards, savedtests,
   and reports pages. The 1600px content cap is inherited from
   the global `.main > *` rule, so .hp-shell only needs to handle
   its own internal gap. */
.main.main--home {
  padding: 18px 24px 48px;
  display: flex; flex-direction: column;
  gap: 18px;
}
@media (max-width: 720px) {
  .main.main--home { padding: 12px 12px 32px; gap: 14px; }
}
/* Define --bg-card and --sh-card at the homepage scope. These
   variables are normally provided by the dashboard surface
   (`.app:has(.dash-header)` block in page.css) and aren't
   available on home.html. Defining them here cascades to every
   `.hp-*` rule below that consumes them, without having to
   edit each rule individually. */
.hp-shell {
  --bg-card: var(--bg-raised, white);
  --sh-card:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.05);
  /* No max-width here — let .main > * cap content at 1600px to
     match every other page. No outer padding either — .main--home
     handles horizontal padding (matching .main--dash). */
  display: flex;
  flex-direction: column;
  gap: 16px;
}
[data-theme="dark"] .hp-shell {
  --sh-card:
    0 1px 2px rgba(0, 0, 0, 0.4),
    0 4px 16px rgba(0, 0, 0, 0.3);
}

/* ── Hero card (own container, floats on page surface) ─────── */
/* Hero is the only "framed" card at the top of the page; every
   body section below it floats independently on the warm page bg.
   That keeps the identity strip visually anchored (welcome, role,
   data-access) while letting the body cards breathe. */
.hp-hero-region {
  position: relative;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--sh-card);
  overflow: hidden;
  /* Brand-tinted gradient distributed across the whole region,
     not just one corner. Two soft blobs (top-right + bottom-left)
     plus a base radial pull from the center give the hero a
     consistent ambient glow without leaving the left side empty. */
  background-image:
    radial-gradient(80% 120% at 100% 0%,
      color-mix(in oklab, var(--brand-500) 18%, var(--bg-card)) 0%,
      var(--bg-card) 55%),
    radial-gradient(60% 100% at 0% 100%,
      color-mix(in oklab, var(--brand-500) 12%, var(--bg-card)) 0%,
      var(--bg-card) 60%);
}
[data-theme="dark"] .hp-hero-region {
  background-image:
    radial-gradient(80% 120% at 100% 0%,
      color-mix(in oklab, var(--brand-500) 28%, var(--bg-card)) 0%,
      var(--bg-card) 55%),
    radial-gradient(60% 100% at 0% 100%,
      color-mix(in oklab, var(--brand-500) 22%, var(--bg-card)) 0%,
      var(--bg-card) 60%);
}
/* ── Hero ──────────────────────────────────────────────────── */
.hp-hero {
  position: relative;
  padding: 10px 20px;
  min-block-size: 80px;
  display: flex;
  align-items: center;
}
.hp-hero-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.hp-hero-bg-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.45;
}
.hp-hero-bg-blob-1 {
  inset-block-start: -50px;
  inset-inline-end: -40px;
  block-size: 180px;
  inline-size: 180px;
  background: var(--brand-500);
}
.hp-hero-bg-blob-2 {
  inset-block-end: -60px;
  inset-inline-start: -20px;
  block-size: 140px;
  inline-size: 140px;
  background: color-mix(in oklab, var(--brand-500) 70%, var(--ok));
  opacity: 0.28;
}
.hp-hero-content {
  position: relative;
  z-index: var(--z-base, 0);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  inline-size: 100%;
}
.hp-hero-left {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 18px;
  min-width: 0;
}
.hp-hero-avatar {
  inline-size: 52px;
  block-size: 52px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--brand-cta);
  display: grid;
  place-items: center;
  color: var(--brand-ink);
  font-size: var(--fsz-h3);
  font-weight: 700;
  letter-spacing: -0.02em;
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 3px var(--line), 0 4px 12px rgba(0,0,0,0.08);
  flex-shrink: 0;
}
.hp-hero-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.hp-hero-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.hp-hero-greet {
  font-size: var(--fsz-h3);
  color: var(--ink);
  font-weight: 500;
  letter-spacing: -0.01em;
  margin: 0;
}
.hp-hero-greet b { font-weight: 700; }
.hp-hero-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 2px; }
.hp-role-pill,
.hp-hero-email {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: var(--fsz-label);
  font-weight: 500;
  border: 1px solid var(--line);
  background: var(--bg-card);
  color: var(--ink-2);
}
.hp-role-pill {
  background: color-mix(in oklab, var(--brand-500) 12%, var(--bg-card));
  border-color: color-mix(in oklab, var(--brand-500) 25%, var(--line));
  color: var(--brand-700);
  font-weight: 600;
}
.hp-role-pill svg { color: var(--brand-500); }
/* Email pill — replaces the institute pill since not every role
   is tied to a single institute (admin / super-admin span the
   tenant / platform respectively). LTR-isolated so the @ symbol
   doesn't reorder in RTL contexts. */
.hp-hero-email {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-caption);
  letter-spacing: 0;
}
.hp-hero-email svg { color: var(--ink-3, var(--ink-2)); }

/* Right side of hero — live clock + date */
.hp-hero-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  flex-shrink: 0;
  text-align: end;
}
[dir="rtl"] .hp-hero-right { align-items: flex-start; text-align: start; }
.hp-hero-clock-time {
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
  font-size: var(--fsz-display);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.02em;
  line-height: 1.1;
}
/* HH:MM — the eye lock. */
.hp-hero-clock-time-main { font-variant-numeric: tabular-nums; }
/* :SS — same size as HH:MM so the digits read as one cohesive
   numeric block (standard digital-clock idiom). A subtle opacity
   reduction (0.85) keeps the ticking second from competing with
   the minute for visual weight without making it look truncated.
   Tabular-nums prevents width jitter between digit changes. */
.hp-hero-clock-time-secs {
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
}
/* AM / PM — small badge after the seconds. */
.hp-hero-clock-time-period {
  font-size: 0.5em;
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-inline-start: 4px;
  align-self: flex-end;
  margin-block-end: 4px;
}
/* GST tag — small de-emphasized time-zone label, mono so the
   letters carry the same precise look as the digits. Aligned to
   the bottom of the time line (not center) so it reads as a
   footnote tag rather than a misaligned chip. */
.hp-hero-clock-zone {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.04em;
  padding: 2px 6px;
  border-radius: 6px;
  background: color-mix(in oklab, var(--ink-2) 6%, transparent);
  border: 1px solid var(--line);
  margin-inline-start: 4px;
  align-self: flex-end;
  margin-block-end: 2px;
}
.hp-hero-clock-date {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  font-weight: 500;
}

/* ── Subsection chrome — each section is its own bordered card.
   Earlier versions used a hairline-divided "stacked sections in
   one container" treatment; now each section has its own border
   + padding + border-radius so the homepage reads as a grid of
   distinct cards rather than a single column of dividers. ──── */
.hp-sub {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--bg-card);
}
.hp-sub-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}
.hp-sub-title-row {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.hp-sub-icon {
  display: inline-grid;
  place-items: center;
  inline-size: 22px;
  block-size: 22px;
  border-radius: 6px;
  background: color-mix(in oklab, var(--brand-500) 10%, var(--bg-card));
  color: var(--brand-700);
  flex-shrink: 0;
}
.hp-sub-title {
  font-size: var(--fsz-body);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.005em;
  margin: 0;
}
.hp-sub-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 20px;
  padding: 0 6px;
  block-size: 18px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--brand-500) 14%, var(--bg-card));
  color: var(--brand-700);
  font-size: var(--fsz-caption);
  font-weight: 700;
}
.hp-sub-link {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--brand-700);
  text-decoration: none;
  /* Unified inline-link treatment — color darken + soft brand bg
     tint on hover, never underline. */
  padding: 2px 6px;
  margin: -2px -6px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background var(--t-fast);
}
.hp-sub-link:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.hp-sub-link:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
/* Header actions slot — holds filter pills (and/or a View-all
   link) on the right side of the subsection header. */
.hp-sub-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.hp-sub-body { display: flex; flex-direction: column; gap: 6px; }
/* Section header right-hand meta line (e.g. "All 11 schools" on
   the Data Access card). Sits next to the title, smaller / mute. */
.hp-sub-meta {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
  letter-spacing: 0.01em;
}
.hp-sub-sep { color: var(--ink-3, var(--ink-2)); opacity: 0.6; }

/* ── Data access card ──────────────────────────────────────── */
/* Per-institute access list. Each <li> is an institute row with a
   mark badge + name on the left and the branch qualifier on the
   right. The list uses a 2-col grid above 720px so 11-institute
   access (super admin) stays compact instead of becoming a tall
   single column. */
.hp-access-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px 12px;
  grid-template-columns: 1fr;
}
@media (min-width: 720px) {
  .hp-access-list { grid-template-columns: 1fr 1fr; }
}

/* Administrative scope list — variant of .hp-access-list rendered
   for administrators (who have platform-wide access by default).
   Each item is a single responsibility chip (icon + label) rather
   than a school-with-branches row. Same grid cadence as the
   schools list so the section reads as a peer treatment, not a
   different surface. */
.hp-admin-scope-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px 12px;
  grid-template-columns: 1fr;
}
@media (min-width: 720px) {
  .hp-admin-scope-list { grid-template-columns: 1fr 1fr; }
}
.hp-admin-scope-item {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: color-mix(in oklab, var(--ink-2) 3%, var(--bg-card));
  min-width: 0;
}
.hp-admin-scope-icon {
  inline-size: 24px;
  block-size: 24px;
  display: inline-grid;
  place-items: center;
  flex-shrink: 0;
  border-radius: 6px;
  background: var(--brand-tint);
  color: var(--brand-700);
}
.hp-admin-scope-label {
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hp-access-item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: color-mix(in oklab, var(--ink-2) 3%, var(--bg-card));
  min-width: 0;
}
.hp-access-inst {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
/* Institute mark badge — short 3-4 letter abbreviation. Brand-tinted
   so the eye finds institute boundaries quickly when scanning a long
   list (super_admin's 11 rows). */
.hp-access-mark {
  display: inline-grid;
  place-items: center;
  min-inline-size: 36px;
  block-size: 22px;
  padding: 0 6px;
  border-radius: 6px;
  background: color-mix(in oklab, var(--brand-500) 12%, var(--bg-card));
  border: 1px solid color-mix(in oklab, var(--brand-500) 20%, var(--line));
  color: var(--brand-700);
  font-size: var(--fsz-label);
  font-weight: 700;
  letter-spacing: 0.02em;
  flex-shrink: 0;
}
.hp-access-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.hp-access-branches {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  flex-shrink: 0;
  text-align: end;
  max-inline-size: 50%;
}
[dir="rtl"] .hp-access-branches { align-items: flex-start; text-align: start; }
.hp-access-count {
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.01em;
}
.hp-access-count.is-all      { color: var(--ok); }
.hp-access-count.is-partial  { color: var(--warn); }
.hp-access-branchlist {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-inline-size: 100%;
}

/* ── Queue (override requests) ─────────────────────────────── */
.hp-queue-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.hp-queue-item { border-radius: 8px; transition: background 150ms ease; }
.hp-queue-item:hover { background: color-mix(in oklab, var(--brand-500) 5%, transparent); }
.hp-queue-link {
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 10px;
  text-decoration: none;
  color: inherit;
}
.hp-queue-priority {
  inline-size: 4px;
  align-self: stretch;
  border-radius: 2px;
  flex-shrink: 0;
}
.hp-queue-priority.is-high { background: var(--err); }
.hp-queue-priority.is-med  { background: var(--warn); }
.hp-queue-priority.is-low  { background: color-mix(in oklab, var(--ink-3, var(--ink-2)) 60%, var(--line)); }
.hp-queue-body { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.hp-queue-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  justify-content: space-between;
}
.hp-queue-title {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-body-sm);
  color: var(--ink);
  min-width: 0;
}
.hp-queue-kind { font-weight: 700; }
.hp-queue-sep { color: var(--ink-3, var(--ink-2)); opacity: 0.6; }
.hp-queue-student { font-weight: 600; }
.hp-queue-pills { display: inline-flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
.hp-queue-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.01em;
  border: 1px solid var(--line);
  background: var(--bg-card);
  white-space: nowrap;
}
.hp-queue-pill-sep { opacity: 0.5; }
.hp-queue-pill-stage { font-weight: 700; }
.hp-queue-pill-status { font-weight: 500; }
.hp-queue-pill.tone-ok {
  background: color-mix(in oklab, var(--ok) 14%, var(--bg-card));
  border-color: color-mix(in oklab, var(--ok) 32%, var(--line));
  color: var(--ok-700, var(--ok));
}
.hp-queue-pill.tone-info {
  background: color-mix(in oklab, var(--brand-500) 12%, var(--bg-card));
  border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line));
  color: var(--brand-700);
}
.hp-queue-pill.tone-warn {
  background: color-mix(in oklab, var(--warn) 14%, var(--bg-card));
  border-color: color-mix(in oklab, var(--warn) 32%, var(--line));
  color: var(--warn);
}
.hp-queue-pill.tone-err {
  background: color-mix(in oklab, var(--err) 14%, var(--bg-card));
  border-color: color-mix(in oklab, var(--err) 32%, var(--line));
  color: var(--err);
}
.hp-queue-pill.tone-brand {
  background: color-mix(in oklab, var(--purple, var(--brand-500)) 14%, var(--bg-card));
  border-color: color-mix(in oklab, var(--purple, var(--brand-500)) 32%, var(--line));
  color: color-mix(in oklab, var(--purple, var(--brand-700)) 70%, var(--ink));
}
.hp-queue-pill.tone-neutral {
  background: color-mix(in oklab, var(--ink-3, var(--ink-2)) 6%, var(--bg-card));
  color: var(--ink-2);
}
.hp-queue-meta {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
}
.hp-queue-meta-sep { opacity: 0.5; }
.hp-queue-link svg:last-child { color: var(--ink-3, var(--ink-2)); flex-shrink: 0; align-self: center; }

/* ── Reports ────────────────────────────────────────────────── */
.hp-reports-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.hp-reports-item { border-radius: 8px; transition: background 150ms ease; }
.hp-reports-item:hover { background: color-mix(in oklab, var(--brand-500) 5%, transparent); }
.hp-reports-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  text-decoration: none;
  color: inherit;
}
.hp-reports-body { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.hp-reports-title { font-size: var(--fsz-body-sm); font-weight: 600; color: var(--ink); }
.hp-reports-meta { font-size: var(--fsz-caption); color: var(--ink-2); }
.hp-reports-empty {
  padding: 12px;
  border-radius: 8px;
  background: color-mix(in oklab, var(--ink-3, var(--ink-2)) 6%, var(--bg-card));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: var(--fsz-body-sm);
  color: var(--ink-2);
}
.hp-reports-empty-cta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--brand-500);
  font-weight: 600;
  text-decoration: none;
}
.hp-reports-empty-cta:hover { text-decoration: underline; }

/* ── Activity feed (shared timeline — Recent + System log) ──
   Reuses the L3 dashboard's `.branch-activity-feed` class so the
   homepage's Recent Activity and Activity Log render identically
   to the Branch dashboard's activity timeline — same vertical
   rail, same colored dots, same who/text/ref/meta layout.

   The only extra thing the homepage adds is wrapping each item
   in an <a> so the row is clickable. Style the link so it sits
   inside the .ive-feed-body slot without breaking the L3 layout. */
.hp-activity .ive-feed-link,
.hp-activitylog .ive-feed-link {
  display: block;
  text-decoration: none;
  color: inherit;
  padding: 4px 6px;
  margin-inline-start: -6px;
  border-radius: 6px;
  flex: 1;
  min-width: 0;
}
.hp-activity .ive-feed-link:hover,
.hp-activitylog .ive-feed-link:hover {
  background: color-mix(in oklab, var(--brand-500) 5%, transparent);
}
/* Cap the activity feed height — without this, a long activity
   feed would push Reports far below the fold. Keep the section
   bounded with internal scroll. Same treatment for the Activity
   Log on Super Admin. */
.hp-activity .hp-sub-body,
.hp-activitylog .hp-sub-body {
  max-block-size: 360px;
  overflow-y: auto;
  /* Inner padding so focus rings on links near the edge aren't
     clipped by the scroll container. */
  padding-inline-end: 4px;
}
/* Timeline dot + line — push both right by 6px so the dot's outer
   ring (box-shadow extends 2px beyond the visible dot) doesn't
   get clipped by the scroll container's overflow: auto. Both the
   dot and the ::before connecting line shift by the same amount,
   keeping their vertical centers aligned at 12px from the item's
   inline-start edge:
     dot:  inset-start 6px + width 12px → center 12px
     line: inset-start 11px + width 2px → center 12px ✓
   The item's text padding bumps from 22px → 28px so the body copy
   keeps a comfortable 10px gap past the dot's right edge. */
.hp-activity .branch-activity-feed .ive-feed-item,
.hp-activitylog .branch-activity-feed .ive-feed-item {
  padding-inline-start: 28px;
}
.hp-activity .branch-activity-feed .ive-feed-dot,
.hp-activitylog .branch-activity-feed .ive-feed-dot {
  inset-inline-start: 6px;
}
.hp-activity .branch-activity-feed .ive-feed-item::before,
.hp-activitylog .branch-activity-feed .ive-feed-item::before {
  inset-inline-start: 11px;
}
/* Empty-filter state when no items match the active filter. */
.hp-feed-empty {
  list-style: none;
  padding: 12px 8px;
  font-size: var(--fsz-body-sm);
  color: var(--ink-2);
  text-align: center;
}

/* ── Alerts (aside col — proactive operational warnings) ──── */
/* Long lists scroll inside the section so the rest of the
   layout stays anchored. Max-height keeps the aside compact
   and the scrollbar shows when alerts overflow. */
.hp-alerts-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  max-block-size: 360px;
  overflow-y: auto;
  padding-inline-end: 2px;
}
.hp-alerts-list:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: 8px;
}
/* Each alert is a plain list row, NOT a colored tile — earlier
   versions tinted the whole row by severity which made a mixed
   list read as one big red/orange block. Now severity is shown
   only via a small colored dot on the left + the severity word
   in the meta line. Rows are separated by a hairline divider. */
.hp-alerts-item {
  border-block-end: 1px solid var(--line);
  transition: background 150ms ease;
}
.hp-alerts-item:last-child { border-block-end: 0; }
.hp-alerts-item:hover { background: color-mix(in oklab, var(--ink-2) 4%, transparent); }
.hp-alerts-link {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 8px;
  text-decoration: none;
  color: inherit;
}
.hp-alerts-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-block-start: 6px;
  background: var(--ink-3, var(--ink-2));
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--ink-3, var(--ink-2)) 25%, transparent);
}
.hp-alerts-item.is-critical .hp-alerts-dot {
  background: var(--err);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--err) 25%, transparent);
}
.hp-alerts-item.is-warn .hp-alerts-dot {
  background: var(--warn);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--warn) 25%, transparent);
}
.hp-alerts-body { flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.hp-alerts-text { font-size: var(--fsz-body-sm); line-height: 1.45; color: var(--ink); font-weight: 500; }
.hp-alerts-meta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--ink-2);
}
.hp-alerts-kind {
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.hp-alerts-kind.is-critical { color: var(--err); }
.hp-alerts-kind.is-warn     { color: var(--warn); }
.hp-alerts-meta-sep { opacity: 0.5; }

/* ── Schedule (aside col) ──────────────────────────────────── */
.hp-schedule-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.hp-schedule-item {
  border-radius: 8px;
  border: 1px solid var(--line);
  transition: border-color 160ms ease, transform 160ms ease;
}
.hp-schedule-item:hover { border-color: color-mix(in oklab, var(--brand-500) 35%, var(--line)); transform: translateY(-1px); }
.hp-schedule-link {
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 8px 10px;
  text-decoration: none;
  color: inherit;
}
.hp-schedule-time {
  font-size: var(--fsz-label);
  font-weight: 700;
  color: var(--brand-700);
  inline-size: 56px;
  flex-shrink: 0;
  align-self: center;
}
.hp-schedule-body { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.hp-schedule-label {
  font-size: var(--fsz-body-sm);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hp-schedule-meta { font-size: var(--fsz-caption); color: var(--ink-2); }
.hp-schedule-item.is-yard   { border-inline-start: 3px solid var(--purple, var(--brand-500)); }
.hp-schedule-item.is-road   { border-inline-start: 3px solid var(--brand-500); }
.hp-schedule-item.is-shift  { border-inline-start: 3px solid color-mix(in oklab, var(--ink-3, var(--ink-2)) 50%, var(--line)); }

/* ── Role picker (demo only) ───────────────────────────────── */
/* ── Demo / dev region (wraps role picker + dev notes) ───────
   Single dashed-bordered container marking the prototype-only
   affordances at the bottom of the homepage. Role picker on top
   (the active control), dev reference below — both inside the
   one frame so they read as a single "this isn't real product
   UI" block. Collapsible via the chevron toggle at top-right;
   collapsed state persists in sessionStorage (per-tab) so a demo
   presenter can hide the scaffolding and have it stay hidden as
   they navigate between pages within the same tab. */
.hp-demo-region {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  border-radius: 14px;
  background: transparent;
  border: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 30%, var(--line));
  margin-block-start: 16px;
  /* Smooth padding collapse so the toggle doesn't snap jarringly.
     Animating padding (and not height) keeps the change localized
     and avoids layout thrash on the surrounding flex container. */
  transition: padding 180ms ease, gap 180ms ease;
}
.hp-demo-region.is-collapsed {
  padding-block: 6px;
  padding-inline: 18px;
  gap: 0;
}
/* Chevron toggle — anchored top-right of the dashed frame,
   sibling-aligned with the "Demo only" pill. Rotates 180° when
   the region is collapsed so the icon visually communicates
   "expand" (points down when collapsed = pull content down)
   vs. "collapse" (points up when expanded = push content up). */
.hp-demo-toggle {
  position: absolute;
  inset-block-start: -14px;
  inset-inline-end: -10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 28px;
  block-size: 28px;
  padding: 0;
  border-radius: 999px;
  background: var(--bg-card);
  border: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 30%, var(--line));
  color: var(--ink-2);
  cursor: pointer;
  transition: transform 180ms ease, background 160ms ease, color 160ms ease;
  font-family: inherit;
}
.hp-demo-toggle:hover {
  background: color-mix(in oklab, var(--ink-2) 6%, var(--bg-card));
  color: var(--ink);
}
.hp-demo-toggle:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
/* When expanded, chevron points UP (rotate the chevDown 180°)
   so it reads as "tap to push content up / collapse". When
   collapsed, leave the icon at its natural orientation (points
   down = "tap to pull content down / expand"). */
.hp-demo-region:not(.is-collapsed) .hp-demo-toggle svg {
  transform: rotate(180deg);
}
.hp-demo-toggle svg {
  transition: transform 200ms ease;
}
/* No [dir="rtl"] override needed — inset-inline-end on the
   .hp-demo-toggle mirrors automatically via the logical property,
   landing the chevron on the inline-end edge in both directions. */
/* "Demo only" pill anchored top-right of the dashed frame so it's
   immediately visible without having to read either inner block. */
.hp-demo-tag {
  position: absolute;
  inset-block-start: -10px;
  inset-inline-end: 16px;
  font-size: var(--fsz-caption);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 10px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--warn) 14%, var(--bg-card));
  color: var(--warn);
  border: 1px solid color-mix(in oklab, var(--warn) 30%, var(--line));
}
[dir="rtl"] .hp-demo-tag {
  /* inset-inline-end already mirrors via the logical property —
     no positional override needed. Only the typographic tweaks
     (no tracking, no uppercase) apply for Arabic. */
  letter-spacing: 0;
  text-transform: none;
}
/* Role picker — now lives inside .hp-demo-region, so the outer
   border/padding it used to carry is gone (the wrapper provides
   them). Stays as a flex column of header + button grid. */
.hp-rolepick {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.hp-rolepick-note {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  margin: 0;
  line-height: 1.5;
  max-inline-size: 760px;
}
/* "Design System" quick-link pill that sits next to the role-picker
   title inside the demo region. Same brand-tinted vocabulary the
   filter pills + level chain use (10% mix bg + 22% mix border +
   brand-600 text) so it reads as a peer affordance to the
   reviewer-only tooling around it, not a primary CTA. Hover lifts
   the bg by one tier (10% → 12%) following the system convention. */
.hp-rolepick-ds-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--brand-600);
  text-decoration: none;
  font-size: var(--fsz-label);
  font-weight: 600;
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  transition: background var(--t-fast), border-color var(--t-fast);
  white-space: nowrap;
}
.hp-rolepick-ds-link:hover {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line));
}
.hp-rolepick-ds-link:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
}
.hp-rolepick-grid {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.hp-rolepick-btn {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 10px;
  text-align: start;
  cursor: pointer;
  transition: border-color 160ms ease, transform 160ms ease;
  font-family: inherit;
}
.hp-rolepick-btn:hover {
  border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line));
  transform: translateY(-1px);
}
.hp-rolepick-btn.is-on {
  border-color: var(--brand-500);
  background: color-mix(in oklab, var(--brand-500) 6%, var(--bg-card));
}
.hp-rolepick-btn-label { font-size: var(--fsz-body-sm); font-weight: 700; color: var(--ink); }
.hp-rolepick-btn-blurb { font-size: var(--fsz-caption); color: var(--ink-2); line-height: 1.4; }

/* ── Dev notes ─────────────────────────────────────────────── */
/* Now lives inside .hp-demo-region beside the role picker, so the
   outer border/radius/padding it used to carry is gone. Keeps its
   own typographic styling (smaller body text, list spacing, code
   chips) since the content is reference material distinct from the
   role picker's interactive grid. A top hairline visually separates
   it from the role picker above. */
.hp-devnotes {
  padding-block-start: 12px;
  border-block-start: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 25%, var(--line));
  font-size: var(--fsz-body-sm);
  color: var(--ink-2);
  line-height: 1.6;
}
.hp-devnotes-title {
  font-size: var(--fsz-body-sm);
  font-weight: 700;
  color: var(--ink);
  margin: 0 0 8px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.hp-devnotes p { margin: 0 0 8px; }
.hp-devnotes p:last-child { margin-block-end: 0; }
.hp-devnotes ul { margin: 4px 0 10px; padding-inline-start: 20px; }
.hp-devnotes li { margin: 2px 0; }
.hp-devnotes code {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-caption);
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  color: var(--ink);
}
/* Design-system reference callout — leading paragraph in the
   developer notes. Brand-tinted left bar + soft background so it
   reads as "important pointer for devs" without shouting. The link
   inside picks up the brand color naturally. */
.hp-devnotes-ds-callout {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 14px;
  margin: 0 0 14px;
  border-radius: 8px;
  background: color-mix(in oklab, var(--brand-500) 5%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand-500) 18%, var(--line));
  border-inline-start: 3px solid color-mix(in oklab, var(--brand-500) 60%, transparent);
  color: var(--ink);
  line-height: 1.55;
}
.hp-devnotes-ds-callout-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--brand-600);
  flex-shrink: 0;
  margin-block-start: 2px;
}
.hp-devnotes-ds-callout a {
  color: var(--brand-700);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.hp-devnotes-ds-callout a:hover { color: var(--brand-800); }
.hp-devnotes-ds-callout a:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ── KPI snapshot strip (top of left column) ─────────────────
   4 boxed mini-KPIs filling the left column's full width. Tiles
   use `flex: 1 1 0` so they share whatever space is available
   equally — at typical desktop widths each tile is ~220-240px,
   on narrower viewports they shrink proportionally. A
   `min-inline-size` floor of 120px stops the shrink before tiles
   become unreadable; once the available width drops below
   ~4 × 120 + gaps, the strip wraps to 2 rows of 2 tiles.
   Tone (ok / warn / err) is conveyed by a thin LEFT stripe so
   the value text stays neutral and never clashes with the brand
   color. */
.hp-kpis {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: stretch;
}
.hp-kpis > .hp-kpi {
  flex: 1 1 0;
  min-inline-size: 120px;
}
.hp-kpi {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-inline-size: 80px;
  /* Each KPI is its own bordered mini-card so the strip never
     conflicts with whatever brand color is active. Tone (ok / warn
     / err) is conveyed by a thin LEFT stripe — not by the value
     text color — so a red or black brand doesn't compete with the
     semantic warn/err tints. The card uses --bg-raised so it sits
     subtly above the hero gradient. */
  padding: 8px 10px 8px 12px;
  background: var(--bg-raised, var(--bg-card));
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}
/* Left tone stripe — drawn via pseudo so the toned color doesn't
   bleed into the value text. Neutral tiles (no tone) just have
   the standard border. */
.hp-kpi.is-ok::before,
.hp-kpi.is-warn::before,
.hp-kpi.is-err::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 3px;
}
.hp-kpi.is-ok::before   { background: var(--ok); }
.hp-kpi.is-warn::before { background: var(--warn); }
.hp-kpi.is-err::before  { background: var(--err); }
.hp-kpi-value {
  font-size: var(--fsz-h1);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.hp-kpi-label {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
  letter-spacing: 0.02em;
}

/* Clock + date — small caption tucked under the KPI strip so the
   right zone reads as: primary (KPIs) + caption (clock). */
.hp-hero-clock {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
}
/* .hp-hero-clock-time canonical definition lives near the hero
   right-zone block above (with main / secs / period spans). The
   duplicate that used to live here was overriding it via cascade
   order and has been removed. */
.hp-hero-clock-sep { opacity: 0.5; }
.hp-hero-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
  flex-shrink: 0;
  max-inline-size: 60%;
}
[dir="rtl"] .hp-hero-right { align-items: flex-start; }

/* ── Body layout — single 2-col grid spanning the entire body.
   Left column carries: primary task surface, recent activity,
   and report schedules. Right column carries the "your day at a
   glance" panel: alerts, secondary section, queue trend.
   Slightly wider right column (1.5fr / 1fr → ~60/40 split)
   so the right cluster reads as a co-equal track, not a thin
   sidebar. Collapses to a single column below 1100px with the
   right column following the left in source order. ─────────── */
.hp-body-2col {
  display: grid;
  gap: 20px;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  align-items: start;
}
.hp-body-left, .hp-body-right {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-width: 0;
}
@media (max-width: 1100px) {
  .hp-body-2col { grid-template-columns: 1fr; }
}

/* All sections render as uniform bordered cards. The primary
   section earns its importance through position (first in the
   left column = natural reading order) rather than extra chrome,
   which kept introducing visual noise as sections got their own
   card frames. */

/* ── Responsive ────────────────────────────────────────────── */
@media (max-width: 720px) {
  .hp-shell { gap: 12px; }
  .hp-hero { padding: 16px 18px; min-block-size: 0; }
  .hp-hero-content { flex-wrap: wrap; }
  .hp-hero-left { gap: 14px; }
  .hp-hero-avatar { inline-size: 56px; block-size: 56px; font-size: var(--fsz-h2); }
  .hp-hero-greet { font-size: var(--fsz-h3); }
  .hp-hero-right { inline-size: 100%; align-items: flex-start; text-align: start; max-inline-size: 100%; }
  [dir="rtl"] .hp-hero-right { align-items: flex-end; text-align: end; }
  /* On mobile, tighten the gap. The flex: 1 1 0 + min-inline-size:
     120px on each tile already handles the wrap-to-2-rows behavior
     automatically once the column width drops below the threshold. */
  .hp-kpis { gap: 8px; }
  .hp-hero-clock-time { font-size: var(--fsz-h1); }
  .hp-queue-title-row { flex-direction: column; align-items: flex-start; }
  .hp-queue-pills { flex-direction: row; }
}

/* ═══════════════════════════════════════════════════════════════
   SYSTEM MONITORING — Phase 1
   Platform-operator-facing health page (super-admin only). Lives
   at systemmonitoring.html. Layout: status banner → 6-tile KPI
   strip → integrations grid → DB + storage pair.
   ═══════════════════════════════════════════════════════════════ */
.main.main--sm {
  padding: 18px 24px 48px;
  display: flex; flex-direction: column;
  gap: 18px;
}
@media (max-width: 720px) {
  .main.main--sm { padding: 12px 12px 32px; gap: 14px; }
}
body:has(.sm-shell) {
  background: var(--bg, var(--bg-muted));
}
.sm-shell {
  --bg-card: var(--bg-raised, white);
  --sh-card:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.05);
  display: flex;
  flex-direction: column;
  gap: 18px;
}
[data-theme="dark"] .sm-shell {
  --sh-card:
    0 1px 2px rgba(0, 0, 0, 0.4),
    0 4px 16px rgba(0, 0, 0, 0.3);
}

/* ── Page head ─────────────────────────────────────────────── */
.sm-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.sm-page-title {
  font-size: var(--fsz-h1);
  font-weight: 700;
  color: var(--ink);
  margin: 0;
  letter-spacing: -0.01em;
}
.sm-page-sub {
  font-size: var(--fsz-body);
  color: var(--ink-2);
  margin: 2px 0 0;
  max-inline-size: 720px;
}
.sm-page-tag {
  font-size: var(--fsz-caption);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--brand-500) 12%, var(--bg-card));
  color: var(--brand-700);
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  align-self: center;
}
[dir="rtl"] .sm-page-tag { letter-spacing: 0; text-transform: none; }

/* ── Status banner ─────────────────────────────────────────── */
/* Headline + uptime meta. Tone (ok/warn/err) drives the left
   accent dot color and the subtle tint of the banner background. */
.sm-banner {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  border-radius: 14px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  box-shadow: var(--sh-card);
}
.sm-banner.is-ok {
  border-color: color-mix(in oklab, var(--ok) 30%, var(--line));
  background: color-mix(in oklab, var(--ok) 4%, var(--bg-card));
}
.sm-banner.is-warn {
  border-color: color-mix(in oklab, var(--warn) 30%, var(--line));
  background: color-mix(in oklab, var(--warn) 5%, var(--bg-card));
}
.sm-banner.is-err {
  border-color: color-mix(in oklab, var(--err) 35%, var(--line));
  background: color-mix(in oklab, var(--err) 6%, var(--bg-card));
}
.sm-banner-dot {
  inline-size: 14px;
  block-size: 14px;
  border-radius: 50%;
  flex-shrink: 0;
}
.sm-banner.is-ok   .sm-banner-dot { background: var(--ok); box-shadow: 0 0 0 4px color-mix(in oklab, var(--ok) 25%, transparent); }
.sm-banner.is-warn .sm-banner-dot { background: var(--warn); box-shadow: 0 0 0 4px color-mix(in oklab, var(--warn) 25%, transparent); }
.sm-banner.is-err  .sm-banner-dot { background: var(--err); box-shadow: 0 0 0 4px color-mix(in oklab, var(--err) 25%, transparent); }
.sm-banner-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sm-banner-headline {
  font-size: var(--fsz-h3);
  font-weight: 700;
  color: var(--ink);
  margin: 0;
}
.sm-banner-meta {
  font-size: var(--fsz-body-sm);
  color: var(--ink-2);
  margin: 0;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px 8px;
}
.sm-banner-meta b { font-weight: 600; color: var(--ink); }
.sm-banner-sep { color: var(--ink-3, var(--ink-2)); opacity: 0.6; }

/* ── KPI strip ─────────────────────────────────────────────── */
/* 6 mini-tiles in a single row that wraps to 3+3 / 2+2+2 on
   narrower viewports. Each tile carries a thin left tone stripe
   (matches the homepage KPI pattern) so the value text stays
   neutral and never clashes with the brand color. */
.sm-kpis {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(6, minmax(0, 1fr));
}
@media (max-width: 1100px) {
  .sm-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .sm-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
}
.sm-kpi {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px 10px 14px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  min-width: 0;
}
.sm-kpi.is-ok::before,
.sm-kpi.is-warn::before,
.sm-kpi.is-err::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 3px;
}
.sm-kpi.is-ok::before   { background: var(--ok); }
.sm-kpi.is-warn::before { background: var(--warn); }
.sm-kpi.is-err::before  { background: var(--err); }
.sm-kpi-value {
  font-size: var(--fsz-h2);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.sm-kpi-label {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  font-weight: 500;
}
.sm-kpi-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
  font-weight: 500;
}

/* ── Section chrome (shared by Integrations, DB, Storage) ───── */
.sm-section {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.sm-card {
  padding: 14px 16px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--sh-card);
}
.sm-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.sm-section-title {
  font-size: var(--fsz-body);
  font-weight: 700;
  color: var(--ink);
  margin: 0;
  letter-spacing: -0.005em;
}
.sm-section-meta {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
}

/* ── Integrations grid ─────────────────────────────────────── */
.sm-int-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.sm-int-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px 16px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--sh-card);
}
/* Subtle left-border tint when the integration is degraded or
   down — the status pill already carries the explicit label, so
   the border is supportive, not the primary signal. */
.sm-int-card.is-warn { border-inline-start: 3px solid var(--warn); }
.sm-int-card.is-err  { border-inline-start: 3px solid var(--err); }
.sm-int-head { display: flex; flex-direction: column; gap: 4px; }
.sm-int-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.sm-int-name {
  font-size: var(--fsz-body);
  font-weight: 700;
  color: var(--ink);
}
.sm-int-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  margin: 0;
}
.sm-int-provider {
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
  margin: 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  letter-spacing: 0;
}

/* `.sm-status-pill` styling MIGRATED to `.pill.is-{ok|warn|err}.is-uppercase`.
   Class names kept as addressing hooks; no styling here. RTL uppercase-strip
   override preserved below since AR digits/text shouldn't be uppercased. */
[dir="rtl"] .pill.is-uppercase.sm-status-pill { letter-spacing: 0; text-transform: none; }

/* 30-day mini-bar uptime strip. Each bar is a thin colored cell;
   the row is a single horizontal flexline so it doesn't push
   the card height around with screen-size changes. */
.sm-int-uptime {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-block-start: 8px;
  border-block-start: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 20%, var(--line));
}
.sm-int-uptime-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.sm-int-uptime-label {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
}
.sm-int-uptime-pct {
  font-size: var(--fsz-label);
  font-weight: 700;
  color: var(--ink);
}
.sm-int-bars {
  display: flex;
  gap: 2px;
  block-size: 18px;
}
.sm-int-bar {
  flex: 1 1 0;
  min-inline-size: 4px;
  border-radius: 2px;
  background: var(--line);
}
.sm-int-bar.is-ok   { background: var(--ok); }
.sm-int-bar.is-warn { background: var(--warn); }
.sm-int-bar.is-err  { background: var(--err); }

.sm-int-incident {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  padding-block-start: 8px;
  border-block-start: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 20%, var(--line));
  display: flex;
  flex-wrap: wrap;
  gap: 4px 6px;
}
.sm-int-incident-when {
  font-weight: 600;
  color: var(--ink);
}
.sm-int-incident-sep { opacity: 0.5; }
.sm-int-incident-desc { color: var(--ink-2); }
.sm-int-incident-dur { color: var(--ink-3, var(--ink-2)); }

/* ── Section pairs (DB+Storage, Perf+Jobs, Sec+Incidents) ──── */
.sm-pair {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  align-items: start;
}
/* `.is-stretch` modifier: force both cards in the row to the
   same height. Used by Perf+Jobs since the chart has a fixed
   minimum height that's taller than the natural jobs height.
   Inside the stretched jobs card the list grows to fill, so the
   queue rows distribute the extra vertical space evenly rather
   than sitting at the top with empty space below them. */
.sm-pair.is-stretch { align-items: stretch; }
.sm-pair.is-stretch > .sm-jobs {
  display: flex;
  flex-direction: column;
}
.sm-pair.is-stretch > .sm-jobs > .sm-jobs-list {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.sm-pair.is-stretch > .sm-jobs > .sm-jobs-list > .sm-jobs-item {
  flex: 1 0 auto;
}

.sm-db-body { display: flex; flex-direction: column; gap: 8px; }
.sm-db-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fsz-body-sm);
}
.sm-db-label { color: var(--ink-2); font-weight: 500; }
.sm-db-value { color: var(--ink); font-weight: 600; }
.sm-db-value.is-ok      { color: var(--ok); }
.sm-db-value.is-warn    { color: var(--warn); }
.sm-db-value.is-err     { color: var(--err); }
.sm-db-num.is-ok        { color: var(--ok); }
.sm-db-num.is-warn      { color: var(--warn); }
.sm-db-num.is-err       { color: var(--err); }
.sm-db-num-sep          { margin: 0 2px; opacity: 0.6; }
.sm-db-num-pct          { color: var(--ink-3, var(--ink-2)); margin-inline-start: 4px; font-weight: 500; }

/* Pool fill bar — visualizes connection pool utilization. Tone
   matches the numeric callout above it (ok/warn/err). */
.sm-db-bar {
  position: relative;
  block-size: 8px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
  margin-block: 2px 6px;
}
.sm-db-bar-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  border-radius: inherit;
  background: var(--ink-3, var(--ink-2));
  transition: inline-size 200ms ease;
}
.sm-db-bar-fill.is-ok   { background: var(--ok); }
.sm-db-bar-fill.is-warn { background: var(--warn); }
.sm-db-bar-fill.is-err  { background: var(--err); }

/* ── Storage card ──────────────────────────────────────────── */
.sm-storage-body { display: flex; flex-direction: column; gap: 12px; }
.sm-storage-bar {
  display: flex;
  block-size: 14px;
  border-radius: 6px;
  overflow: hidden;
  background: var(--line);
}
.sm-storage-seg {
  block-size: 100%;
  transition: inline-size 200ms ease;
}
.sm-storage-recordings { background: var(--brand-500); }
.sm-storage-photos     { background: color-mix(in oklab, var(--brand-500) 60%, var(--ink-3, var(--ink-2))); }
.sm-storage-logs       { background: color-mix(in oklab, var(--brand-500) 30%, var(--ink-3, var(--ink-2))); }
.sm-storage-free       { background: var(--line); }
.sm-storage-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.sm-storage-row {
  display: grid;
  grid-template-columns: 16px 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: var(--fsz-body-sm);
}
.sm-storage-swatch {
  inline-size: 12px;
  block-size: 12px;
  border-radius: 3px;
  background: var(--line);
  flex-shrink: 0;
}
.sm-storage-row-label { color: var(--ink-2); }
.sm-storage-row.is-free .sm-storage-row-label,
.sm-storage-row.is-free .sm-storage-row-value { color: var(--ink-3, var(--ink-2)); font-style: italic; }
.sm-storage-row-value { color: var(--ink); font-weight: 600; }
.sm-storage-pct {
  font-weight: 700;
  margin-inline-start: 4px;
}
.sm-storage-pct.is-ok   { color: var(--ok); }
.sm-storage-pct.is-warn { color: var(--warn); }
.sm-storage-pct.is-err  { color: var(--err); }

/* ── Permission-denied state ───────────────────────────────── */
.sm-denied,
.access-denied {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 48px 32px;
  background: var(--bg-card);
  border: 1px dashed var(--line);
  border-radius: 14px;
  max-inline-size: 480px;
  margin: 48px auto;
}
.sm-denied-title,
.access-denied-title {
  font-size: var(--fsz-h3);
  font-weight: 700;
  color: var(--ink);
  margin: 0;
}
.sm-denied-body,
.access-denied-body {
  font-size: var(--fsz-body);
  color: var(--ink-2);
  margin: 0;
  line-height: 1.5;
}
.sm-denied-link,
.access-denied-link {
  display: inline-flex;
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--brand-500);
  text-decoration: none;
  margin-block-start: 20px;
}
.sm-denied-link:hover,
.access-denied-link:hover { text-decoration: underline; }
.access-denied-icon {
  inline-size: 56px;
  block-size: 56px;
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand-tint);
  color: var(--brand-700);
  margin-block-end: 6px;
}

/* ═══════════════════════════════════════════════════════════════
   SYSTEM MONITORING — Phase 2 (perf chart + jobs + security +
   incidents + live polling controls)
   ═══════════════════════════════════════════════════════════════ */

/* ── Live polling bar (page head right slot) ───────────────── */
.sm-livebar {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  box-shadow: var(--sh-card);
  align-self: center;
  flex-wrap: wrap;
}
.sm-live-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--ok); color: var(--ok);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--ok) 25%, transparent);
  animation: pill-dot-pulse 2s ease-out infinite;
}
.sm-live-dot.is-paused {
  background: var(--ink-3, var(--ink-2));
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--ink-3, var(--ink-2)) 18%, transparent);
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  .sm-live-dot { animation: none; }
}
.sm-live-label {
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.02em;
}
.sm-live-time {
  font-size: var(--fsz-caption);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  padding-inline: 4px;
}
.sm-live-btn {
  font: inherit;
  font-size: var(--fsz-caption);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 10px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--brand-500) 10%, var(--bg-card));
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  color: var(--brand-700);
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease;
}
.sm-live-btn:hover {
  background: color-mix(in oklab, var(--brand-500) 18%, var(--bg-card));
}
.sm-live-btn:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
.sm-live-btn[aria-pressed="true"] {
  background: color-mix(in oklab, var(--warn) 12%, var(--bg-card));
  border-color: color-mix(in oklab, var(--warn) 30%, var(--line));
  color: var(--warn);
}
[dir="rtl"] .sm-live-label,
[dir="rtl"] .sm-live-btn { letter-spacing: 0; text-transform: none; }

/* ── Performance chart ─────────────────────────────────────── */
.sm-perf {
  position: relative;
  padding: 14px 16px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--sh-card);
}
.sm-perf-legend {
  display: inline-flex;
  gap: 12px;
  flex-wrap: wrap;
  font-size: var(--fsz-caption);
  color: var(--ink-2);
}
.sm-perf-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.sm-perf-swatch {
  display: inline-block;
  inline-size: 10px;
  block-size: 10px;
  border-radius: 2px;
  background: var(--ink-3, var(--ink-2));
}
/* p50 → softer brand tint; p95 → mid brand; p99 → warn red; rpm →
   neutral accent. Visually orders the lines from coolest (p50) to
   most alerting (p99), so the eye sees severity at a glance. */
.sm-perf-swatch.sm-perf-p50 { background: color-mix(in oklab, var(--brand-500) 50%, var(--ink-2)); }
.sm-perf-swatch.sm-perf-p95 { background: var(--brand-500); }
.sm-perf-swatch.sm-perf-p99 { background: var(--warn); }
.sm-perf-swatch.sm-perf-rpm { background: color-mix(in oklab, var(--ok) 80%, var(--ink-2)); }

/* Chart layout — 2-col CSS grid:
     col 1 (.sm-perf-yaxis)   — fixed-width HTML axis labels
     col 2 (.sm-perf-canvas)  — SVG geometry; height pinned in
                                 pixels so the chart can't grow
                                 unboundedly tall as the container
                                 widens.
   Two body rows (latency canvas + throughput canvas) plus an
   x-axis label row at the bottom. */
.sm-perf-grid-wrap {
  margin-block-start: 12px;
  display: grid;
  grid-template-columns: 56px 1fr;
  /* Pinned heights kept compact so the perf card lands close to
     the jobs card height. The .sm-pair stretch rule below
     handles the final size match. */
  grid-template-rows: 140px 50px 18px;
  gap: 4px 8px;
  align-items: stretch;
}
@media (max-width: 720px) {
  .sm-perf-grid-wrap {
    grid-template-columns: 44px 1fr;
    grid-template-rows: 120px 44px 16px;
  }
}
/* HTML Y-axis labels — positioned absolutely by % so each label
   centers on its grid line. Fixed CSS font-size means labels stay
   crisp at any chart width (the old SVG <text> was stretched
   horizontally by preserveAspectRatio="none"). */
.sm-perf-yaxis {
  position: relative;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-label);
  line-height: 1;
  color: var(--ink-3, var(--ink-2));
}
.sm-perf-yaxis-label {
  position: absolute;
  inset-inline-end: 0;
  white-space: nowrap;
  transform: translateY(-50%);
}
[dir="rtl"] .sm-perf-yaxis-label {
  inset-inline-end: 0;
}
/* X-axis HTML row sits beneath the throughput canvas. Each label
   anchored by its inline-start % into the row. */
.sm-perf-xaxis {
  position: relative;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-label);
  line-height: 1;
  color: var(--ink-3, var(--ink-2));
}
.sm-perf-xaxis-label {
  position: absolute;
  inset-block-start: 0;
  transform: translateX(-50%);
  white-space: nowrap;
}
[dir="rtl"] .sm-perf-xaxis-label {
  transform: translateX(50%);
}

/* Canvas — positioning context for the SVG, also catches the
   pointer events for hover. block-size is inherited from the
   grid template rows so the SVG can't grow tall. */
.sm-perf-canvas {
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  cursor: crosshair;
}
.sm-perf-svg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  display: block;
  pointer-events: none; /* canvas owns mousemove */
}
.sm-perf-grid {
  stroke: var(--line);
  stroke-dasharray: 2 4;
}
.sm-perf-grid.is-x { stroke-dasharray: 1 5; opacity: 0.5; }
.sm-perf-line {
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2;
}
.sm-perf-line.sm-perf-p50 { stroke: color-mix(in oklab, var(--brand-500) 50%, var(--ink-2)); }
.sm-perf-line.sm-perf-p95 { stroke: var(--brand-500); }
.sm-perf-line.sm-perf-p99 { stroke: var(--warn); }
.sm-perf-rpm-line { stroke: color-mix(in oklab, var(--ok) 70%, var(--ink-2)); stroke-width: 1.6; }
.sm-perf-area {
  fill: color-mix(in oklab, var(--ok) 18%, transparent);
}
.sm-perf-cursor {
  stroke: var(--ink-3, var(--ink-2));
  stroke-dasharray: 3 3;
}
.sm-perf-dot {
  stroke: var(--bg-card);
  stroke-width: 1.5;
}
.sm-perf-dot.sm-perf-p50 { fill: color-mix(in oklab, var(--brand-500) 50%, var(--ink-2)); }
.sm-perf-dot.sm-perf-p95 { fill: var(--brand-500); }
.sm-perf-dot.sm-perf-p99 { fill: var(--warn); }
.sm-perf-dot.sm-perf-rpm-dot { fill: color-mix(in oklab, var(--ok) 70%, var(--ink-2)); }

/* Hover tooltip — anchored top-right of the perf section, not
   following the cursor. Keeps the chart bounds clean and avoids
   tooltip-clipping headaches on small screens. */
.sm-perf-tip {
  position: absolute;
  inset-block-start: 12px;
  inset-inline-end: 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  box-shadow: var(--sh-card);
  font-size: var(--fsz-caption);
  color: var(--ink);
  pointer-events: none;
  min-inline-size: 140px;
}
.sm-perf-tip-head {
  font-weight: 700;
  color: var(--ink);
  padding-block-end: 4px;
  border-block-end: 1px dashed var(--line);
  margin-block-end: 2px;
  font-family: var(--font-mono, ui-monospace, monospace);
}
.sm-perf-tip-row {
  display: grid;
  grid-template-columns: 12px 1fr auto;
  align-items: center;
  gap: 6px;
}
.sm-perf-tip-row b {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ── Background jobs list ──────────────────────────────────── */
.sm-jobs-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sm-jobs-item {
  display: grid;
  grid-template-columns: 12px 1fr auto auto;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  background: color-mix(in oklab, var(--ink-2) 3%, var(--bg-card));
  border: 1px solid var(--line);
  transition: background 160ms ease;
}
.sm-jobs-item:hover {
  background: color-mix(in oklab, var(--brand-500) 5%, var(--bg-card));
}
.sm-jobs-item.is-warn { border-inline-start: 3px solid var(--warn); }
.sm-jobs-item.is-err  { border-inline-start: 3px solid var(--err); }
.sm-jobs-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--ink-3, var(--ink-2));
}
.sm-jobs-dot.is-ok   { background: var(--ok); box-shadow: 0 0 0 2px color-mix(in oklab, var(--ok)   25%, transparent); }
.sm-jobs-dot.is-warn { background: var(--warn); box-shadow: 0 0 0 2px color-mix(in oklab, var(--warn) 25%, transparent); }
.sm-jobs-dot.is-err  { background: var(--err); box-shadow: 0 0 0 2px color-mix(in oklab, var(--err)  25%, transparent); }
.sm-jobs-name {
  font-size: var(--fsz-body-sm);
  font-weight: 600;
  color: var(--ink);
}
.sm-jobs-pending,
.sm-jobs-avg {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: var(--fsz-body-sm);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.sm-jobs-pending-label,
.sm-jobs-avg-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
  font-weight: 500;
}
.sm-jobs-pending b,
.sm-jobs-avg b { font-weight: 700; }

/* ── Security counter grid ─────────────────────────────────── */
.sm-sec-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
  /* Container-responsive: 4 tiles fit when the card is wide
     enough (~600px+), drops to 2 then 1 as the card narrows.
     Replaces the old viewport-based media queries so the grid
     adapts cleanly when the security card lives in a half-
     width .sm-pair column on a wide viewport. */
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.sm-sec-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  background: color-mix(in oklab, var(--ink-2) 3%, var(--bg-card));
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}
.sm-sec-tile.is-ok::before,
.sm-sec-tile.is-warn::before,
.sm-sec-tile.is-err::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 3px;
}
.sm-sec-tile.is-ok::before   { background: var(--ok); }
.sm-sec-tile.is-warn::before { background: var(--warn); }
.sm-sec-tile.is-err::before  { background: var(--err); }
.sm-sec-value {
  font-size: var(--fsz-h2);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.sm-sec-label {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  font-weight: 500;
}
.sm-sec-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}
.sm-sec-delta {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.sm-sec-delta.is-up   { color: var(--err); }
.sm-sec-delta.is-down { color: var(--ok); }
.sm-sec-open {
  display: inline-flex;
  font-size: var(--fsz-caption);
  font-weight: 700;
  color: var(--warn);
  padding-inline-start: 2px;
}
.sm-sec-sub-text { color: var(--ink-3, var(--ink-2)); }

/* ── Recent incidents timeline ─────────────────────────────── */
/* Reuses .branch-activity-feed dot+line styling. The only
   override is to give the timestamp pill in front of each row
   a fixed slot so the lines align nicely. */
.sm-incidents-list { padding-inline-start: 0; }
.sm-incidents-time {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-2);
  background: color-mix(in oklab, var(--ink-2) 6%, var(--bg-card));
  padding: 1px 6px;
  border-radius: 4px;
  margin-inline-end: 4px;
}
.sm-incidents-ongoing {
  color: var(--warn);
  font-weight: 600;
}
.sm-incidents-empty {
  font-size: var(--fsz-body-sm);
  color: var(--ink-2);
  padding: 8px 4px;
  margin: 0;
}
/* The .ive-feed-dot has a box-shadow ring that gets clipped if
   the parent has overflow constraints. Give the incidents
   timeline the same dot-shift treatment as the homepage
   activity feed for visual consistency. */
.sm-incidents-list .ive-feed-item { padding-inline-start: 28px; padding-block: 10px; }
.sm-incidents-list .ive-feed-dot { inset-inline-start: 6px; inset-block-start: 16px; }
.sm-incidents-list .ive-feed-item::before {
  inset-inline-start: 11px;
  inset-block-start: 22px;
  inset-block-end: -22px;
}

/* ── Responsive — Phase 2 ──────────────────────────────────── */
@media (max-width: 720px) {
  .sm-livebar { padding: 4px 10px; gap: 6px; }
  .sm-perf-tip {
    /* On small screens the tooltip would crowd the chart head, so
       reposition to follow the cursor area at the top of the SVG. */
    position: static;
    margin-block-start: 8px;
  }
  .sm-jobs-item {
    grid-template-columns: 12px 1fr;
    grid-template-rows: auto auto;
  }
  .sm-jobs-name { grid-column: 2; grid-row: 1; }
  .sm-jobs-pending,
  .sm-jobs-avg { grid-column: 2; }
}

/* ═══════════════════════════════════════════════════════════════
   SYSTEM MONITORING — Yards connectivity (Phase 2 extension)
   ═══════════════════════════════════════════════════════════════ */

/* ── Counter tiles (6: yard counts + aggregate cross-yard) ──── */
/* 6 KPI tiles in a single row on wide viewports. First three are
   yard-status counters (Healthy / Degraded / Offline); next
   three are aggregate cross-yard metrics derived live each
   render (Vehicles online / Video pending / Network traffic).
   Drops to 3 cols then 2 then 1 as the viewport narrows. */
.sm-yard-counts {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(6, minmax(0, 1fr));
}
@media (max-width: 1280px) {
  .sm-yard-counts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .sm-yard-counts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 380px) {
  .sm-yard-counts { grid-template-columns: 1fr; }
}
.sm-yard-count {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
  background: color-mix(in oklab, var(--ink-2) 3%, var(--bg-card));
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}
.sm-yard-count::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 3px;
}
.sm-yard-count.is-ok::before      { background: var(--ok); }
.sm-yard-count.is-warn::before    { background: var(--warn); }
.sm-yard-count.is-err::before     { background: var(--err); }
.sm-yard-count.is-neutral::before { background: var(--ink-3, var(--ink-2)); }
.sm-yard-count-value {
  font-size: var(--fsz-h2);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
}
/* "/" separator inside "online/total" — softer than the main
   numbers so the value reads as two related numbers, not a
   fraction expression. */
.sm-yard-count-sep {
  color: var(--ink-3, var(--ink-2));
  font-weight: 500;
  margin: 0 1px;
}
/* Unit suffix (e.g., "Mbps") — smaller + dimmer than the value. */
.sm-yard-count-unit {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink-2);
  letter-spacing: 0;
}
.sm-yard-count-label {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  font-weight: 500;
}

/* ── Yard table — uses the standard .data-table primitive ─── */
/* The yards list is rendered as a real <table className="data-table">
   so it inherits the project-wide sortable-header chrome (icon +
   hint chevrons + active sort arrow, hover state, sticky thead).
   Wrapped in .sm-yard-table-scroll so the wide table can overflow
   horizontally on narrow viewports without breaking the section. */
.sm-yard-table-scroll {
  overflow-x: auto;
  overflow-y: visible;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-card);
  max-block-size: 480px;
  overflow-clip-margin: 32px;
}
.sm-yard-table {
  inline-size: 100%;
  min-inline-size: 920px;  /* keeps columns readable; scroll below */
  border-collapse: collapse;
  /* `fixed` makes the browser honor the proportional widths set
     on the <colgroup>; without this the auto algorithm
     redistributes by content and the traffic column ends up
     squished. */
  table-layout: fixed;
}
.sm-yard-table thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: color-mix(in oklab, var(--ink-2) 4%, var(--bg-card));
  border-block-end: 1px solid var(--line);
  padding: 0;
  text-align: start;
}
.sm-yard-table tbody td {
  padding: 10px 14px;
  border-block-end: 1px solid var(--line);
  font-size: var(--fsz-body-sm);
  vertical-align: middle;
}
.sm-yard-table tbody tr:last-child td { border-block-end: 0; }
.sm-yard-table tbody tr:hover {
  background: color-mix(in oklab, var(--brand-500) 4%, var(--bg-card));
}
.sm-yard-table tbody tr.is-warn { background: color-mix(in oklab, var(--warn) 3%, var(--bg-card)); }
.sm-yard-table tbody tr.is-err  { background: color-mix(in oklab, var(--err)  4%, var(--bg-card)); }

/* Per-column widths come from the <colgroup> on the table; only
   text alignment quirks live here per-cell. */
.sm-yard-cell-lastseen { text-align: end; }
[dir="rtl"] .sm-yard-cell-lastseen { text-align: start; }

/* Status cell — dot + labeled pill ("Online" / "Degraded" /
   "Offline"). The dot is the quick-scan signal; the text label
   makes the value explicit so users don't have to interpret
   color semantics. The cell is a <td>, so display rules apply
   to the td itself and its inline children. */
.sm-yard-cell-status {
  white-space: nowrap;
}
.sm-yard-cell-status .sm-yard-dot,
.sm-yard-cell-status .sm-yard-status-text {
  vertical-align: middle;
}
.sm-yard-cell-status .sm-yard-dot { margin-inline-end: 6px; }
.sm-yard-dot {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 50%;
  background: var(--ink-3, var(--ink-2));
  flex-shrink: 0;
}
.sm-yard-dot.is-ok   { background: var(--ok);   box-shadow: 0 0 0 2px color-mix(in oklab, var(--ok)   25%, transparent); }
.sm-yard-dot.is-warn { background: var(--warn); box-shadow: 0 0 0 2px color-mix(in oklab, var(--warn) 25%, transparent); }
.sm-yard-dot.is-err  { background: var(--err);  box-shadow: 0 0 0 2px color-mix(in oklab, var(--err)  25%, transparent); }
.sm-yard-status-text {
  font-size: var(--fsz-body-sm);
  font-weight: 600;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}
.sm-yard-status-text.is-ok   { color: var(--ok); }
.sm-yard-status-text.is-warn { color: var(--warn); }
.sm-yard-status-text.is-err  { color: var(--err); }

/* Branch cell — name + institute mark, both inline */
.sm-yard-cell-branch {
  white-space: nowrap;
}
.sm-yard-cell-branch .sm-yard-branch-name,
.sm-yard-cell-branch .sm-yard-inst {
  vertical-align: middle;
}
.sm-yard-branch-name {
  font-weight: 600;
  color: var(--ink);
  margin-inline-end: 8px;
}
.sm-yard-inst {
  font-size: var(--fsz-caption);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 1px 6px;
  border-radius: 4px;
  background: color-mix(in oklab, var(--brand-500) 10%, var(--bg-card));
  color: var(--brand-700);
  border: 1px solid color-mix(in oklab, var(--brand-500) 20%, var(--line));
  flex-shrink: 0;
}
[dir="rtl"] .sm-yard-inst { letter-spacing: 0; text-transform: none; }

/* Metric cells — vehicles / video / traffic. Each <td> contains
   a <div class="sm-yard-stack"> wrapper that stacks the primary
   value and the caption-tinted secondary label vertically. The
   flex layout MUST live on the wrapping div (not the <td>) —
   `display: flex` on a <td> breaks it out of the table-cell
   layout and merges columns together. */
.sm-yard-stack {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.sm-yard-vehicles-value,
.sm-yard-video-pending,
.sm-yard-traffic-value {
  font-size: var(--fsz-body-sm);
  font-weight: 500;
  color: var(--ink);
}
.sm-yard-vehicles-value b,
.sm-yard-video-pending b,
.sm-yard-traffic-value b { font-weight: 700; }
.sm-yard-cell-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
  font-weight: 500;
}
.sm-yard-cell-vehicles.is-warn .sm-yard-vehicles-value b { color: var(--warn); }
.sm-yard-cell-vehicles.is-err  .sm-yard-vehicles-value b { color: var(--err); }
.sm-yard-cell-video.is-warn .sm-yard-video-pending b { color: var(--warn); }
.sm-yard-cell-video.is-err  .sm-yard-video-pending b { color: var(--err); }
.sm-yard-na { color: var(--ink-3, var(--ink-2)); font-style: italic; }

/* Traffic cell — sparkline on top spans the full cell width;
   numeric meta (current Mbps + peak) below it. The 350px cell
   gives the spark ~320px to render in, so short-term spikes are
   visually obvious. */
.sm-yard-cell-traffic .sm-yard-spark {
  inline-size: 100%;
  block-size: 32px;
  display: block;
}
.sm-yard-traffic-meta {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
}
.sm-yard-traffic-sep { color: var(--ink-3, var(--ink-2)); opacity: 0.5; }
.sm-yard-spark-line {
  stroke: var(--ok);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sm-yard-spark.is-warn .sm-yard-spark-line { stroke: var(--warn); }
.sm-yard-spark.is-err  .sm-yard-spark-line { stroke: var(--err); }

/* Last-seen cell */
.sm-yard-cell-lastseen {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.sm-yard-table tbody tr.is-err .sm-yard-cell-lastseen {
  color: var(--err);
  font-weight: 600;
}

/* ── Responsive ────────────────────────────────────────────── */
/* Below 1100px the table still scrolls horizontally inside its
   wrapper — min-inline-size on the table keeps columns readable
   and the user can swipe horizontally to see the rest. This
   matches the Saved Tests / Reports tables' behavior. */
@media (max-width: 1100px) {
  .sm-yard-cell-traffic .sm-yard-spark { block-size: 28px; }
}

/* ═══════════════════════════════════════════════════════════════
   SYSTEM MONITORING — System users (Phase 2 extension)
   ═══════════════════════════════════════════════════════════════ */

/* ── 2-col section layout: KPI tiles | role distribution ──── */
/* Outer split inside the Users section. Equal 50/50 columns so
   both halves get the same visual weight on wide viewports.
   Collapses to a single column below 1100px so both blocks get
   full width on tablet / mobile. */
.sm-users-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}
@media (max-width: 1100px) {
  .sm-users-grid {
    grid-template-columns: 1fr;
    gap: 20px;
  }
}

/* ── KPI tile grid inside the left side of .sm-users-grid ─────
   3-col internal layout so 6 tiles render as a clean 3+3. Each
   tile gets ~240px on a typical desktop, matching the role
   list's typography on the right. Yards uses its own 6-col
   grid; this one is users-specific. */
.sm-users-counts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 1100px) {
  /* When the outer grid collapses, KPIs span full width — keep
     a 3-col tile layout if there's room; drop to 2 below 720. */
  .sm-users-counts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .sm-users-counts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 380px) {
  .sm-users-counts { grid-template-columns: 1fr; }
}

/* Role distribution column — sits on the right side of the
   .sm-users-grid 2-col layout. No top divider needed (the
   adjacent KPI grid + the gap between columns already separate
   the two blocks visually); the divider was a holdover from
   when the role list lived BELOW the KPI strip. When the outer
   grid collapses to single column on tablet/mobile, a top
   divider IS added back to restore the visual separation. */
.sm-users-roles {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
@media (max-width: 1100px) {
  .sm-users-roles {
    margin-block-start: 6px;
    padding-block-start: 14px;
    border-block-start: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 25%, var(--line));
  }
}
.sm-users-roles-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.sm-users-roles-title {
  font-size: var(--fsz-body-sm);
  font-weight: 700;
  color: var(--ink);
  margin: 0;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
[dir="rtl"] .sm-users-roles-title { letter-spacing: 0; text-transform: none; }

/* List rows replace the old stacked-bar + separate legend. Each
   row has its own inline mini-bar (a track filled to `pct%`) so
   the visual magnitude cue lives on each role's line. */

/* Per-row swatch — small color tag at the inline-start of each
   row. NO default background here; the role-specific colors
   below MUST win the cascade for both swatches AND fills to
   share the same hue. */
.sm-users-role-swatch {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 3px;
}

/* Inline progress-bar track per row. 6px tall, rounded ends,
   fills horizontally as the role's share of total grows.
   `inline-size` on the fill is set inline by JSX as a percent. */
.sm-users-role-track {
  block-size: 6px;
  background: var(--line);
  border-radius: 3px;
  overflow: hidden;
  min-inline-size: 0;
}
.sm-users-role-fill {
  display: block;
  block-size: 100%;
  inline-size: 0%;
  transition: inline-size 240ms ease;
}

/* Per-role colors — 5 distinct hues (no longer a brand ramp,
   which made examiner/supervisor/senior all blend into shades
   of blue). Palette walks the color wheel cool → warm by role
   privilege: blue → teal → green → orange → red. Lightness +
   chroma are pinned so the palette stays harmonious; varying
   the hue is what creates clear separation between roles.
   Applied to BOTH the bar segment (.sm-users-role-seg) and the
   legend dot (.sm-users-role-swatch) via the shared
   .sm-users-role-{id} class on each. */
.sm-users-role-examiner          { background: oklch(0.62 0.16 250); }  /* blue   */
.sm-users-role-supervisor        { background: oklch(0.70 0.14 195); }  /* teal   */
.sm-users-role-senior_supervisor { background: oklch(0.72 0.16 145); }  /* green  */
.sm-users-role-section_manager   { background: oklch(0.72 0.16 110); }  /* lime   */
.sm-users-role-director          { background: oklch(0.72 0.17  85); }  /* gold   */
.sm-users-role-administrator     { background: oklch(0.74 0.16  55); }  /* orange */
.sm-users-role-super_admin       { background: oklch(0.60 0.20  18); }  /* red    */

/* List — one full-width row per role with: swatch · label ·
   count · inline mini-bar · percent. Single column (one row per
   role) so the layout reads top-down by importance, scales
   linearly to many roles, and never wraps awkwardly. */
.sm-users-role-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: var(--fsz-body-sm);
}
.sm-users-role-item {
  display: grid;
  /* swatch · label · count (right-aligned numeric) · bar track
     (flex grow, primary visual weight) · percent (compact right-
     aligned). The label gets a comfortable min-width so short
     labels don't squish the bar; the bar grows to fill the row. */
  grid-template-columns: 12px minmax(140px, 1fr) 56px minmax(120px, 2fr) 44px;
  align-items: center;
  gap: 10px;
}
.sm-users-role-label {
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sm-users-role-count {
  color: var(--ink);
  font-weight: 700;
  text-align: end;
}
[dir="rtl"] .sm-users-role-count { text-align: start; }
.sm-users-role-pct {
  color: var(--ink-3, var(--ink-2));
  font-size: var(--fsz-caption);
  font-weight: 500;
  text-align: end;
}
[dir="rtl"] .sm-users-role-pct { text-align: start; }

/* Compact reflow on narrow viewports — drop the count column
   inline; the bar still carries the magnitude and the percent
   carries the precise value. The count is the redundant one
   when space is tight. */
@media (max-width: 720px) {
  .sm-users-role-item {
    grid-template-columns: 12px minmax(0, 1fr) minmax(80px, 1.6fr) 40px;
  }
  .sm-users-role-count { display: none; }
}

/* ═══════════════════════════════════════════════════════════════
   HOMEPAGE — Super-admin snapshot cards
   Two compact read-only cards that replace the per-user
   chrome (Data Access, Schedule) on the super-admin homepage:
     .hp-snap-sys   — platform status snapshot (uptime/incidents)
     .hp-snap-users — user-population snapshot (counts + roles)
   Both use the .hp-sub chrome (header + body) — just add
   snapshot-specific inner styling.
   ═══════════════════════════════════════════════════════════════ */

/* Top banner inside the System status snapshot — tone-tinted
   horizontal strip with a colored dot + a short label
   ("All systems operational" / "Minor degradation" / "Active
   incident"). Mirrors the bigger banner on the monitoring
   page in a compact form. */
.hp-snap-banner {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 8px;
  background: color-mix(in oklab, var(--ok) 4%, var(--bg-card));
  border: 1px solid color-mix(in oklab, var(--ok) 30%, var(--line));
  font-size: var(--fsz-body-sm);
  color: var(--ink);
  font-weight: 600;
  inline-size: fit-content;
  margin-block-end: 6px;
}
.hp-snap-banner.is-warn {
  background: color-mix(in oklab, var(--warn) 5%, var(--bg-card));
  border-color: color-mix(in oklab, var(--warn) 30%, var(--line));
}
.hp-snap-banner.is-err {
  background: color-mix(in oklab, var(--err) 6%, var(--bg-card));
  border-color: color-mix(in oklab, var(--err) 35%, var(--line));
}
.hp-snap-banner-dot {
  inline-size: 9px;
  block-size: 9px;
  border-radius: 50%;
  flex-shrink: 0;
}
.hp-snap-banner-dot.is-ok   { background: var(--ok);   box-shadow: 0 0 0 3px color-mix(in oklab, var(--ok)   25%, transparent); }
.hp-snap-banner-dot.is-warn { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in oklab, var(--warn) 25%, transparent); }
.hp-snap-banner-dot.is-err  { background: var(--err);  box-shadow: 0 0 0 3px color-mix(in oklab, var(--err)  25%, transparent); }

/* Metric grid — 2 cols by default, drops to 1 on narrow.
   Each metric is value-on-top, label-below, kept compact so
   the snapshot card stays short. */
.hp-snap-metrics {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px 14px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hp-snap-sys .hp-snap-metrics {
  /* Sys snapshot has 6 metrics — 3 cols looks better. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 720px) {
  .hp-snap-metrics,
  .hp-snap-sys .hp-snap-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.hp-snap-metric {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.hp-snap-metric-value {
  font-size: var(--fsz-h3);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.hp-snap-metric-label {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-weight: 500;
}
.hp-snap-metric.is-warn .hp-snap-metric-value { color: var(--warn); }
.hp-snap-metric.is-err  .hp-snap-metric-value { color: var(--err); }
.hp-snap-metric.is-ok   .hp-snap-metric-value { color: var(--ink); }

/* Role distribution mini — bottom of the Users snapshot.
   Compact stacked bar showing the top 3 roles + an "Other"
   bucket, with a small legend below. The full breakdown lives
   on the monitoring page. Per-role colors come from
   .sm-users-role-{id} (shared with the monitoring page so
   the visual identity stays consistent). */
.hp-snap-roles {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-block-start: 8px;
  padding-block-start: 10px;
  border-block-start: 1px dashed color-mix(in oklab, var(--ink-3, var(--ink-2)) 25%, var(--line));
}
.hp-snap-roles-bar {
  display: flex;
  block-size: 8px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--line);
}
.hp-snap-roles-seg {
  block-size: 100%;
  transition: flex-grow 200ms ease;
}
.hp-snap-roles-seg-other { background: color-mix(in oklab, var(--ink-3, var(--ink-2)) 35%, var(--line)); }
.hp-snap-roles-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px 12px;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  font-size: var(--fsz-caption);
  color: var(--ink-2);
}
.hp-snap-roles-item {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  align-items: center;
  gap: 6px;
}
.hp-snap-roles-swatch {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 2px;
}
.hp-snap-roles-label { color: var(--ink-2); }
.hp-snap-roles-count { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ═══════════════════════════════════════════════════════════════
   CHROME v2 — SideNav (desktop) + MobileTopStrip (mobile only)
   The legacy TopNav and the short-lived v1 .topbar are both
   superseded by this block. Pages opt in with `app--sidenav` on
   the outer .app wrapper.

   Layout grid (logical, RTL-aware via CSS grid writing-mode):
     - 1 column [sidenav | main] on desktop (no top row)
     - 1 row,  2 columns
     - On mobile (≤768px): collapses to single column, sidenav
       becomes a fixed off-canvas drawer triggered by the
       MobileTopStrip hamburger.

   Three responsive modes:
     ≥ 1101px (default)         — full sidenav, 256px
     721–1100 OR
       html[data-sidenav-collapsed] — rail, 64px
     ≤ 720px                      — drawer (off-canvas)

   The `data-sidenav-collapsed` attribute is set BEFORE first
   paint by the per-page inline hydration script, so the user's
   persisted preference takes effect without a width flash. The
   SideNav React component mirrors the same attribute on toggle.
   ═══════════════════════════════════════════════════════════════ */

:root {
  --sidenav-w-full: 256px;
  --sidenav-w-rail: 64px;
  --mobile-strip-h: 48px;
  --sidenav-item-h: 44px;        /* WCAG 2.5.5 touch target */
  --sidenav-child-h: 40px;
  --sidenav-row-gap: 4px;        /* nav-tree gap (tight rhythm for primary nav) */
  --sidenav-footer-gap: 10px;    /* looser — preferences feel distinct */
  --sidenav-header-gap: 12px;    /* room between brand mark and notif */
  --sidenav-easing-out: cubic-bezier(0.0, 0.0, 0.2, 1);
  --sidenav-easing-in:  cubic-bezier(0.4, 0.0, 1.0, 1);
}

/* The rail column width is driven by a TYPED custom property so it can be
   transitioned. Animating grid-template-columns directly is unreliable (a
   `<length> 1fr` track list doesn't interpolate cleanly — that was the
   original snap/jank). A registered `<length>` interpolates frame-by-frame
   and the grid track follows it, so the rail GLIDES smoothly. Crucially the
   sidenav's LEFT edge never moves — only this right-hand track changes — so
   the logo / icons / avatar keep their x-positions and the label fade stays
   in sync. Snapped off under prefers-reduced-motion (chrome block); inert at
   mobile (grid is overridden to 1fr there). */
@property --sidenav-col {
  syntax: "<length>";
  inherits: false;
  initial-value: 256px;
}

.app--sidenav {
  display: grid;
  --sidenav-col: var(--sidenav-w-full);
  grid-template-columns: var(--sidenav-col) 1fr;
  min-block-size: 100vh;
  min-block-size: 100dvh;
  background: var(--bg);
  /* ~220ms decelerate, synced with the label fade. */
  transition: --sidenav-col 220ms var(--sidenav-easing-out);
}
:root[data-sidenav-collapsed] .app--sidenav {
  --sidenav-col: var(--sidenav-w-rail);
}

/* ── Skip link — first focusable; visually hidden until focused */
.skip-link {
  position: fixed;
  inset-inline-start: 8px;
  inset-block-start: 8px;
  transform: translateY(-150%);
  padding: 8px 14px;
  background: var(--brand-700, var(--brand-600));
  color: var(--brand-ink);
  border-radius: 6px;
  font-size: var(--fsz-body-sm);
  font-weight: 600;
  text-decoration: none;
  z-index: var(--z-toast);
  transition: transform 140ms var(--sidenav-easing-out);
}
.skip-link:focus { transform: translateY(0); outline: 2px solid var(--brand-500); outline-offset: 2px; }

/* ── SIDENAV (the whole left-rail / drawer) ──────────────────── */

.sidenav {
  grid-column: 1;
  grid-row: 1;
  position: sticky;
  top: 0;
  block-size: 100vh;
  block-size: 100dvh;
  background: var(--bg-raised);
  border-inline-end: 1px solid var(--line);
  display: grid;
  grid-template-rows: auto 1fr auto;
  z-index: calc(var(--z-sticky) - 10);
  /* overflow: visible so floating UI (data-tip tooltips, profile
     popover) can escape the rail edge in collapsed mode. The
     scrollable region is .sidenav-content, which has its own
     overflow-y: auto and stays contained. */
  overflow: visible;
}
/* Grid items default to min-inline-size: auto which lets their
   intrinsic content size push them past the container width.
   In rail mode (64px) this caused header/content/footer to
   compute as 92px and overflow horizontally. Forcing 0 lets
   the grid track govern the inline size. */
.sidenav-header,
.sidenav-content,
.sidenav-footer { min-inline-size: 0; }

/* Custom scrollbar inside the content zone — understated. */
.sidenav-content {
  overflow-y: auto;
  overflow-x: hidden;
  padding-block: 8px;
  padding-inline: 10px;
  display: flex;
  flex-direction: column;
  gap: var(--sidenav-row-gap);
}
.sidenav-content::-webkit-scrollbar { inline-size: 6px; }
.sidenav-content::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--ink-3, var(--ink-2)) 30%, transparent);
  border-radius: 3px;
}

/* ── HEADER ZONE: brand mark + notifications row. Generous
   padding above + below the brand so the logo has breathing
   room from the rail's top edge and from the notifications row
   beneath it. ─────────────────────────────────────────────── */
.sidenav-header {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding-block-start: 16px;
  padding-block-end: 12px;
  padding-inline: 10px;
  border-block-end: 1px solid var(--line);
  gap: var(--sidenav-header-gap);
}
/* Brand block — display only, not clickable. */
.sidenav-brand {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  inline-size: 100%;
  min-inline-size: 0;
  /* No inline padding: the logo sits flush at the header's content edge in
     BOTH states, so it never shifts horizontally when the rail collapses.
     (The header's own 10px padding still gives it breathing room.) */
  padding-inline: 0;
  pointer-events: none;
}
/* Tenant logo — pinned to 40px tall in BOTH modes so the brand
   reads consistently as the user toggles. The SVG itself scales
   width to keep its native aspect ratio. */
.sidenav-brand-mark {
  display: block;
  inline-size: 100%;
  block-size: 40px;
  /* Tenant logo in the expanded sidenav — tenant-driven via --logo-tenant-*.
     (Collapsed rail swaps to the tenant ICON below.) */
  background-image: var(--logo-tenant-light, url('assets/tenants/pl/PL-logo-light.svg'));
  background-repeat: no-repeat;
  background-position: left center;
  background-size: auto 40px;     /* fixed pixel box (not contain) so the logo never rescales/shifts when the rail narrows — mirrors iVP */
}
[data-theme="dark"] .sidenav-brand-mark {
  background-image: var(--logo-tenant-dark, url('assets/tenants/pl/PL-logo-dark.svg'));
}
[dir="rtl"] .sidenav-brand-mark { background-position: right center; }

/* Rail-handle edge toggle — centered ON the sidenav border so
   the round button straddles the seam symmetrically (8px inside
   the rail, 8px outside in the canvas). 18px-wide handle = 9px
   each side of the border. Always shows a thin bar; hover reveals
   the round button (brand-colored, white chevron). */
.sidenav-rail-handle {
  position: absolute;
  inset-block: 0;
  /* Anchor to the rail's inline-end OUTER edge (including the
     1px border) so the handle's center sits EXACTLY on the
     border line. CSS inset-inline-end is relative to the
     padding box, so we offset by (handle-half + border-width)
     = 9 + 1 = 10px. 18px wide handle → 9px each side of the
     border line. */
  inset-inline-end: -10px;
  inline-size: 18px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  z-index: 2;
}
.sidenav-rail-handle:focus { outline: none; }
.sidenav-rail-handle:focus-visible .sidenav-rail-handle-btn {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
/* Always-visible thin bar — sits exactly on the border line
   (handle is centered on the border, so 50% of handle = border). */
.sidenav-rail-handle-bar {
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(50% - 1.5px);
  inline-size: 3px;
  background: color-mix(in oklab, var(--ink) 10%, transparent);
  border-radius: 4px;
  transition: background 160ms var(--sidenav-easing-out),
              inline-size 160ms var(--sidenav-easing-out);
}
/* Round chevron button — opacity 0 by default, fades in on
   hover. Pinned to the TOP of the rail (aligned with the
   tenant logo). Rest state is muted; hover escalates to
   brand-500/white. */
.sidenav-rail-handle-btn {
  position: absolute;
  /* Centered ON the divider line between the header and the
     nav tree (y=125). Button is 22×22 → top = 125 - 11 = 114.
     22px diameter + 1px border mirrors the test-canvas rail
     handle and the maneuver P/F badge, keeping small circular
     affordances at a consistent visual weight across the app. */
  inset-block-start: 114px;
  inset-inline-start: 50%;
  transform: translateX(-50%) scale(0.88);
  opacity: 0;
  inline-size: 22px;
  block-size: 22px;
  border-radius: 50%;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-2);
  box-shadow: 0 2px 10px color-mix(in oklab, var(--ink) 14%, transparent);
  pointer-events: none;            /* parent button is the click target */
  transition: opacity 160ms var(--sidenav-easing-out),
              transform 160ms var(--sidenav-easing-out),
              background 120ms var(--sidenav-easing-out),
              border-color 120ms var(--sidenav-easing-out),
              color 120ms var(--sidenav-easing-out),
              box-shadow 120ms var(--sidenav-easing-out);
}
/* Reveal ONLY on direct hover of the 16px rail-handle zone or
   on keyboard focus. Hovering nav items / flyouts inside the
   sidenav no longer triggers the bar+button — keeps the chrome
   quiet when the user is actively navigating. */
.sidenav-rail-handle:hover .sidenav-rail-handle-bar,
.sidenav-rail-handle:focus-visible .sidenav-rail-handle-bar {
  background: var(--brand-500);
  /* 3px on hover (down from 4px) to match the test-canvas rail
     handle and the video overlay resize line — keeps all
     "drag/grab this edge" affordances at the same visual weight. */
  inline-size: 3px;
}
.sidenav-rail-handle:hover .sidenav-rail-handle-btn,
.sidenav-rail-handle:focus-visible .sidenav-rail-handle-btn {
  opacity: 1;
  transform: translateX(-50%) scale(1);
  border-color: var(--brand-500);
  box-shadow: 0 4px 14px color-mix(in oklab, var(--brand-700, var(--brand-500)) 25%, transparent);
}
/* RTL fixes:
   1. `translateX(-50%)` doesn't auto-flip with `inset-inline-start: 50%`,
      so in RTL the button drifts ~half its width to the LEFT of where
      it should sit (anchor flips but transform stays viewport-relative).
      Flip the X translate sign to keep the button centered on its anchor.
   2. The chevron SVG paths are direction-fixed (always point LTR). Flip
      the icon horizontally so "collapse" still points toward the
      sidenav (which is on the RIGHT in RTL) and "expand" toward content. */
[dir="rtl"] .sidenav-rail-handle-btn { transform: translateX(50%) scale(0.88); }
[dir="rtl"] .sidenav-rail-handle:hover .sidenav-rail-handle-btn,
[dir="rtl"] .sidenav-rail-handle:focus-visible .sidenav-rail-handle-btn {
  transform: translateX(50%) scale(1);
}
[dir="rtl"] .sidenav-rail-handle-btn svg { transform: scaleX(-1); }
/* Direct hover (mouse over the handle itself) — escalate to
   brand-500 / white chevron, same way a nav item lights up
   when hovered. */
.sidenav-rail-handle:hover .sidenav-rail-handle-btn {
  background: var(--brand-cta);
  border-color: var(--brand-cta);
  color: var(--brand-ink);
  box-shadow: 0 6px 18px color-mix(in oklab, var(--brand-700, var(--brand-500)) 45%, transparent);
}
/* Touch / coarse pointer: keep the button always visible since
   there's no real "hover" on touch. */
@media (pointer: coarse) {
  .sidenav-rail-handle-btn {
    opacity: 1;
    transform: translateX(-50%) scale(1);
  }
}
/* Tablet (721–1100): both the bar AND the chevron button stay
   quiet by default — revealed only on hover or keyboard focus of
   the 18px handle zone. This override applies at any pointer type
   (fine OR coarse) — explicitly overriding the global
   `@media (pointer: coarse)` always-on rule for the button just
   above. Touch tablet users tap the rail-handle's 18px hit area
   blind; the visible rail edge (the 1px border between sidenav
   and content) is the natural affordance seam.
   The existing global hover rules (`.sidenav-rail-handle:hover ...`)
   reveal both elements on hover/focus — no need to redefine here.
   ──
   Source order matters: this block is placed AFTER `@media (pointer:
   coarse) { .sidenav-rail-handle-btn { opacity: 1 } }` so the
   cascade resolves in this rule's favour at tablet width. */
@media (min-width: 721px) and (max-width: 1100px) {
  .sidenav-rail-handle-bar { opacity: 0; }
  .sidenav-rail-handle-btn {
    opacity: 0;
    transform: translateX(-50%) scale(0.88);
  }
}
/* Reduced motion: skip the reveal animation, snap to visible
   on hover. */
@media (prefers-reduced-motion: reduce) {
  .sidenav-rail-handle-bar,
  .sidenav-rail-handle-btn { transition: none; }
}
/* Mirror the chevron in RTL so it still points the direction
   the rail will go on click. */
[dir="rtl"] .sidenav-rail-handle-btn svg { transform: scaleX(-1); }

/* Notifications row — sits just below the brand row, looks like
   a normal nav item but has its own popover. */
.sidenav-notif { position: relative; }
.sidenav-notif-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  inline-size: 100%;
  min-block-size: var(--sidenav-item-h);
  padding-block: 0;
  padding-inline: 12px;
  border: none;
  background: transparent;
  border-radius: 8px;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--fsz-body);   /* 14px — design system body baseline */
  font-weight: 500;
  text-align: start;
  cursor: pointer;
  position: relative;
  /* Asymmetric hover transition: smooth IN (~80ms fade) but
     INSTANT OUT — the :hover rule defines the in-transition;
     the base rule has none so removing :hover snaps the color
     back immediately. This kills the "trail" of multiple items
     appearing hovered when the cursor sweeps across the rail. */
  transition: background 0ms, color 0ms;
}
/* Hover surface for every interactive row in the sidenav.
   brand-500 @ 10% sits clearly below the active pill (12%) so the
   visual hierarchy reads as: rest → hover (preview) → active
   (statement) — without being so faint it disappears against the
   sidenav surface, which was the failure mode at 6%. Previously
   used `var(--brand-tint)` which mixes brand-700 @ 10% against an
   opaque --bg surface — that ended up MORE saturated than the
   transparent active pill in the sidenav, inverting the
   hierarchy. */
.sidenav-notif-btn:hover {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--ink);
}
.sidenav-notif-btn:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }
.sidenav-notif-btn.has-unread { color: var(--ink); }
.sidenav-notif-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--err, #e11d48);
  margin-inline-start: auto;
  flex-shrink: 0;
  box-shadow: 0 0 0 2px var(--bg-raised);
}

/* Override the legacy .notif-panel anchoring when it's hosted
   inside the sidenav. Default rule (designed for the topbar)
   puts the panel to the LEFT of the bell with
   `inset-inline-end: -8px` — which inside the sidenav lands
   off-screen on the left. Inside .sidenav-notif we anchor the
   panel to the RIGHT of the bell instead, so it pops out of
   the sidenav into the main content area. RTL handled
   automatically by inset-inline-* logical properties. */
.sidenav-notif .notif-panel {
  inset-inline-start: calc(100% + 8px);
  inset-inline-end: auto;
  top: 0;
}
/* On mobile (drawer) the panel reverts to the legacy
   placement — the drawer is itself an off-canvas overlay so
   the panel can hang below the bell as normal. */
@media (max-width: 720px) {
  .sidenav-notif .notif-panel {
    inset-inline-start: auto;
    inset-inline-end: -8px;
    top: calc(100% + 10px);
  }
}

/* ── NAV ITEMS — top-level + group children ─────────────────── */
/* Typography per design system:
     top-level + groups → --fs-body (14px / 400, 600 when active)
     group children     → --fs-body-sm (13px / 400)
   Icon column is 20px wide to align with the 14px body baseline
   and keep optical balance with the new 44px touch targets. */
.sidenav-item {
  position: relative;             /* anchors the .is-active::before strip */
  display: flex;
  align-items: center;
  gap: 12px;
  inline-size: 100%;
  min-block-size: var(--sidenav-item-h);
  padding-block: 0;
  padding-inline: 12px;
  border-radius: 8px;
  border: none;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--fsz-body);    /* 14px — design system body */
  font-weight: 500;
  text-decoration: none;
  text-align: start;
  cursor: pointer;
  /* Asymmetric hover transition: smooth IN (~80ms fade) but
     INSTANT OUT — the :hover rule defines the in-transition;
     the base rule has none so removing :hover snaps the color
     back immediately. This kills the "trail" of multiple items
     appearing hovered when the cursor sweeps across the rail. */
  transition: background 0ms, color 0ms;
}
.sidenav-item:hover {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--ink);
}
.sidenav-item:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }
.sidenav-item.is-active {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  color: var(--brand-600);
  font-weight: 600;
}
/* Full-height brand strip on the leading edge of every active row
   (top-level leaves AND `.is-child` rows inside expanded groups).
   Same 3px brand-500 strip vocabulary used in the institute rail
   (.client-active-bar) so both surfaces share an identical "you
   are here" affordance. The strip's leading corners match the
   row's border-radius (8px) so it doesn't poke outside the pill.
   Always layered ABOVE the brand-tint pill (z=auto on a position:
   absolute child sits on top of the row's background paint). */
.sidenav-item.is-active::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--brand-500);
  border-radius: 8px 0 0 8px;
  pointer-events: none;
}
[dir="rtl"] .sidenav-item.is-active::before {
  border-radius: 0 8px 8px 0;
}
/* Active state: icon inherits the brand text color so label + icon
   read as a single, unified brand-colored row (not text in one shade,
   icon in another). */
.sidenav-item.is-active .sidenav-item-icon { color: inherit; }
.sidenav-item.is-child {
  /* Children share the same row height + font size + icon size
     as top-level items — the only difference is the indent so
     the hierarchy still reads. */
  padding-inline-start: 40px;
  color: var(--ink-2);
}
/* Re-assert active-state color for child items — `.is-child`
   declares `color: var(--ink-2)` which would otherwise win the
   cascade against `.is-active` (same specificity, later wins). */
.sidenav-item.is-active.is-child { color: var(--brand-600); }
.sidenav-item-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 20px;              /* aligned with 14px body type */
  flex-shrink: 0;
  color: var(--ink-2);
}
.sidenav-item-label {
  flex: 1 1 auto;
  min-inline-size: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Smooth fade + inline-size collapse when the rail closes, so labels
     sweep off rather than blink out (was display:none). iVP's timings. */
  transition:
    opacity 160ms var(--sidenav-easing-out),
    max-inline-size 200ms var(--sidenav-easing-out),
    margin 200ms var(--sidenav-easing-out);
  max-inline-size: 280px;
  opacity: 1;
}

/* ── GROUPS — expand/collapse inline (full mode) ─────────────── */
.sidenav-group { display: flex; flex-direction: column; }
.sidenav-group-head {
  position: relative;             /* anchors the has-active-child::before strip */
  display: flex;
  align-items: center;
  gap: 12px;
  inline-size: 100%;
  min-block-size: var(--sidenav-item-h);
  padding-block: 0;
  padding-inline: 12px;
  border-radius: 8px;
  border: none;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--fsz-body);     /* 14px — same tier as leaf items */
  font-weight: 500;
  text-align: start;
  cursor: pointer;
  /* Asymmetric hover transition: smooth IN (~80ms fade) but
     INSTANT OUT — the :hover rule defines the in-transition;
     the base rule has none so removing :hover snaps the color
     back immediately. This kills the "trail" of multiple items
     appearing hovered when the cursor sweeps across the rail. */
  transition: background 0ms, color 0ms;
}
.sidenav-group-head:hover,
.sidenav-group-head.is-flyout-open {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--ink);
}
/* When the flyout is open, the trigger's icon stays "lit" so
   the user can see which icon owns the open flyout. */
.sidenav-group-head.is-flyout-open .sidenav-item-icon { color: var(--ink); }
.sidenav-group-head:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }
.sidenav-group-chev {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 16px;
  flex-shrink: 0;
  color: var(--ink-3, var(--ink-2));
  /* transform = the expand/collapse rotate; opacity/max-inline-size/margin
     = the rail-collapse fade (the chevron sweeps off with the labels). */
  transition:
    transform 180ms var(--sidenav-easing-out),
    opacity 160ms var(--sidenav-easing-out),
    max-inline-size 200ms var(--sidenav-easing-out),
    margin 200ms var(--sidenav-easing-out);
  max-inline-size: 16px;
  opacity: 1;
}
.sidenav-group.is-expanded .sidenav-group-chev { transform: rotate(90deg); }
[dir="rtl"] .sidenav-group-chev { transform: rotate(180deg); }
[dir="rtl"] .sidenav-group.is-expanded .sidenav-group-chev { transform: rotate(90deg); }

/* When a group contains the current page, the group head reads
   as brand-colored in BOTH states:
   - Expanded (children visible): just brand color on text + icon
     so the parent still signals "this is where you are".
   - Collapsed (children hidden): also gets the active background
     pill so the user can spot at a glance which collapsed group
     owns their page. */
.sidenav-group.has-active-child .sidenav-group-head {
  color: var(--brand-600);
}
.sidenav-group.has-active-child .sidenav-group-head .sidenav-item-icon {
  color: inherit;
}
.sidenav-group.has-active-child.is-collapsed .sidenav-group-head {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  font-weight: 600;
}
/* Full-height brand strip on the active group head, matching the
   one on `.sidenav-item.is-active`. Applied only when the group is
   COLLAPSED — when expanded, the active child renders its own
   strip, so the parent group head doesn't double-stripe. */
.sidenav-group.has-active-child.is-collapsed .sidenav-group-head::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--brand-500);
  border-radius: 8px 0 0 8px;
  pointer-events: none;
}
[dir="rtl"] .sidenav-group.has-active-child.is-collapsed .sidenav-group-head::before {
  border-radius: 0 8px 8px 0;
}

/* RAIL-MODE ACTIVE-CHILD TREATMENT
   When the sidenav is in rail mode (manual collapse OR tablet
   auto-rail 721-1100), the group's children are hidden regardless
   of its own `.is-expanded` / `.is-collapsed` state — only the
   group head is visible. In that case the head should ALWAYS get
   the full active treatment (pill background + leading strip) when
   one of its children is the current page, otherwise the user
   sees only the brand-tint icon color with no background pill,
   making the "you are here" affordance much weaker than the
   top-level leaf items get.

   Three triggers — duplicated because @media + attribute can't
   share one selector list:
     1. :root[data-sidenav-collapsed]  → manual collapse
     2. @media tablet auto-rail        → 721–1100 viewport
     3. @media mobile drawer           → ≤720 (sidenav is fixed
        drawer; same visual semantics when open) */
:root[data-sidenav-collapsed] .sidenav-group.has-active-child .sidenav-group-head {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  font-weight: 600;
}
:root[data-sidenav-collapsed] .sidenav-group.has-active-child .sidenav-group-head::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--brand-500);
  border-radius: 8px 0 0 8px;
  pointer-events: none;
}
[dir="rtl"]:root[data-sidenav-collapsed] .sidenav-group.has-active-child .sidenav-group-head::before {
  border-radius: 0 8px 8px 0;
}
@media (min-width: 721px) and (max-width: 1100px) {
  .sidenav-group.has-active-child .sidenav-group-head {
    background: color-mix(in oklab, var(--brand-500) 12%, transparent);
    font-weight: 600;
  }
  .sidenav-group.has-active-child .sidenav-group-head::before {
    content: '';
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    width: 3px;
    background: var(--brand-500);
    border-radius: 8px 0 0 8px;
    pointer-events: none;
  }
  [dir="rtl"] .sidenav-group.has-active-child .sidenav-group-head::before {
    border-radius: 0 8px 8px 0;
  }
}

.sidenav-group-children {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
  max-block-size: 320px;
  opacity: 1;
  margin-block-start: 2px;
  transition: max-block-size 200ms var(--sidenav-easing-out),
              opacity 160ms var(--sidenav-easing-out),
              margin-block-start 200ms var(--sidenav-easing-out);
}
.sidenav-group.is-collapsed .sidenav-group-children {
  max-block-size: 0;
  opacity: 0;
  margin-block-start: 0;
  /* Exit is faster than enter (60-70%) — feels responsive. */
  transition: max-block-size 130ms var(--sidenav-easing-in),
              opacity 100ms var(--sidenav-easing-in),
              margin-block-start 130ms var(--sidenav-easing-in);
}

/* ── FOOTER ZONE: theme + lang + profile. Looser gap than the
   nav tree so the three preference rows feel distinct. ───── */
.sidenav-footer {
  border-block-start: 1px solid var(--line);
  padding-block: 12px;
  padding-inline: 10px;
  display: flex;
  flex-direction: column;
  gap: var(--sidenav-footer-gap);
  background: var(--bg-raised);
}

/* Lang + Theme footer rows render with .sidenav-item chrome so
   they look identical to primary nav rows (no pill, no border,
   no card — same hover bg, same touch target, same typography).
   .sidenav-tool-row is a semantic marker for the two switcher
   rows in the footer; styling comes from .sidenav-item. */
.sidenav-tool-row[data-tip]::after {
  /* The default [data-tip]::after positions ABOVE the element.
     For sidenav rows we want the tooltip to anchor to the right
     (out of the rail, into the main content area) — same idea as
     the notif panel override above. */
  bottom: auto;
  left: calc(100% + 6px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
}
[dir="rtl"] .sidenav-tool-row[data-tip]::after {
  left: auto;
  right: calc(100% + 6px);
  transform: translateY(-50%);
}

/* Theme + Lang toolbar pair — both buttons live on ONE row in
   expanded mode, stacked vertically in rail mode. Each button
   shows the DESTINATION state of the switch ("switch to X"). */
.sidenav-tools-pair {
  display: flex;
  gap: 8px;
  inline-size: 100%;
}
.sidenav-tools-pair > .sidenav-tool-icon { flex: 1 1 0; min-inline-size: 0; }

.sidenav-tool-icon {
  inline-size: 100%;
  min-block-size: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding-block: 4px;
  padding-inline: 8px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  cursor: pointer;
  transition: background 0ms, color 0ms, border-color 0ms;
}
.sidenav-tool-icon:hover {
  /* Same vocabulary as `.sidenav-item:hover` — brand-500 @ 10%
     transparent — so theme + lang CTAs share the rest of the
     sidenav's hover rhythm instead of the more-saturated
     `var(--brand-tint)` token (which mixes brand-700 against
     an opaque --bg and ends up heavier than the 12% active
     pill, inverting the visual hierarchy). */
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--ink);
  border-color: var(--line-2, var(--line));
}
.sidenav-tool-icon:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.sidenav-tool-icon .sidenav-flag {
  inline-size: 20px;
  block-size: 14px;
}
.sidenav-tool-iconslot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 18px;
  flex-shrink: 0;
}
.sidenav-tool-label {
  font-size: var(--fsz-body-sm);   /* 13px — matches body-sm */
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Default: full label visible, short hidden. Lang button only —
   theme labels stay full in expanded mode. */
.sidenav-tool-label--short { display: none; }
/* In collapsed-rail mode the LangPill drops to a single Arabic
   glyph ("ع") rather than the full "عربي" which would ellipsis-
   truncate inside the 64px rail. At 13px a lone Arabic letter
   reads spindly next to a latin "EN" sibling; bumping the
   AR-only short label to 16px restores optical balance.
   `lang="ar"` is set by React on the short span only when the
   destination language is Arabic, so this rule scopes correctly.
   `line-height: 1.4` + `overflow: visible` keeps the Arabic
   descender (the loop on ع that sits below the baseline) from
   being clipped by the parent's `overflow: hidden` + `line-height: 1`
   ellipsis defaults — those only matter for the full text label,
   not for a single glyph. */
.sidenav-tool-label--short[lang="ar"] {
  /* 14px — matches body text size. Keeps the single Arabic glyph
     readable in the 64px rail without dominating the row visually
     against the latin "EN" sibling and the surrounding icons. */
  font-size: var(--fsz-body);
  line-height: 1.4;
  overflow: visible;
  padding-block-end: 2px;
}

/* Place the data-tip tooltip ABOVE the tool button (not beside
   it). The side placement was overlapping the adjacent pill in
   the footer pair when both Theme and Lang are visible. Anchor
   to the icon's leading edge so the tooltip grows toward the
   trailing side — this keeps it inside the viewport even in
   rail mode where the sidenav hugs the leading edge of the
   canvas. Mirrors in RTL via the logical inset properties. */
.sidenav-tool-icon[data-tip]::after {
  bottom: calc(100% + 6px);
  top: auto;
  inset-inline-start: 0;
  inset-inline-end: auto;
  transform: none;
  white-space: nowrap;
}

/* Rail mode: stack the pair vertically + adapt each button to
   its compact form. Lang: hide flag + swap full label for short
   glyph ("English" → "EN"). Theme: hide label, icon-only. */
@media (min-width: 721px) and (max-width: 1100px) {
  /* Footer pills (theme + lang) follow the same expanded-override
     pattern as the rest of the tablet rail-visual rules — when the
     user has manually expanded (`data-sidenav-expanded`), these
     rules deactivate and the pills render side-by-side with full
     labels and the flag, matching the desktop full layout. */
  :root:not([data-sidenav-expanded]) .sidenav-tools-pair { flex-direction: column; }
  :root:not([data-sidenav-expanded]) .sidenav-tool-icon.is-lang .sidenav-flag { display: none; }
  :root:not([data-sidenav-expanded]) .sidenav-tool-icon.is-lang .sidenav-tool-label--full { display: none; }
  :root:not([data-sidenav-expanded]) .sidenav-tool-icon.is-lang .sidenav-tool-label--short { display: inline; }
  :root:not([data-sidenav-expanded]) .sidenav-tool-icon.is-theme .sidenav-tool-label { display: none; }
}
/* Theme + lang pill rail-collapsed treatment — vertical stack,
   flag hidden, full label hidden, short glyph shown. Scoped to
   ≥721px (same rationale as the other [data-sidenav-collapsed]
   rules above): the mobile drawer must always render the FULL
   pills (flag + full label) regardless of the user's prior
   desktop rail choice. Without this scope the user opens the
   burger and sees the compact lang glyph "ع" / icon-only theme
   button — different from the standard expanded menu. */
@media (min-width: 721px) {
  :root[data-sidenav-collapsed] .sidenav-tools-pair { flex-direction: column; }
  :root[data-sidenav-collapsed] .sidenav-tool-icon.is-lang .sidenav-flag { display: none; }
  :root[data-sidenav-collapsed] .sidenav-tool-icon.is-lang .sidenav-tool-label--full { display: none; }
  :root[data-sidenav-collapsed] .sidenav-tool-icon.is-lang .sidenav-tool-label--short { display: inline; }
  :root[data-sidenav-collapsed] .sidenav-tool-icon.is-theme .sidenav-tool-label { display: none; }
}

/* Inline lang-tagged text (like "عربي" inside the English UI
   or "EN" inside the Arabic UI) should render in the proper
   font for that language. Uses the design-system Arabic stack
   --font-ar (IBM Plex Sans Arabic) when lang="ar".
   Global (not scoped to .sidenav): any element marked with
   lang="ar" anywhere in the app — sidenav lang-pill, login
   page lang switch, breadcrumb chips, footer labels, etc. —
   picks up the Arabic font automatically. Any author adding
   an inline Arabic glyph inside an English UI just adds
   `lang="ar"` to the wrapping span and the typography stays
   on-system. */
[lang="ar"] { font-family: var(--font-ar); }

/* Compact-mode lang glyph — "عربي" or "EN" as a text mark
   instead of a country flag (clearer at icon size, and reads
   as the destination language at a glance). Sized to match the
   icon slot used by other nav rows. */
.sidenav-lang-glyph {
  font-size: var(--fsz-body-sm);  /* 13px — fits the 20px icon column */
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1;
  color: inherit;
}

/* Country flag — fixed slot so labels align across both lang
   variants. 20×14 matches the 4:3 flag aspect. */
.sidenav-flag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 20px;
  block-size: 14px;
  border-radius: 2px;
  overflow: hidden;
  flex-shrink: 0;
}
.sidenav-flag svg { inline-size: 100%; block-size: 100%; }

/* Profile row at the very bottom — borderless, no card chrome.
   48px tall so the 40×40 avatar has 4px breathing room top +
   bottom (matches the spacing of other footer rows). */
.sidenav-profile { position: relative; }
.sidenav-profile-trigger {
  display: flex;
  align-items: center;
  gap: 12px;
  inline-size: 100%;
  min-block-size: 48px;
  padding-block: 4px;
  /* 2px inline-start centers the 40px avatar in the 64px rail when collapsed
     (footer 10 + 2 = 12 = (64-40)/2), and the avatar holds that exact x when
     expanded too — so it doesn't jump on toggle. 8px inline-end keeps the
     chevron off the edge in the expanded row. */
  padding-inline: 2px 8px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: background 0ms;
}
.sidenav-profile-trigger:hover {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
}
.sidenav-profile-trigger:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.sidenav-profile.is-open .sidenav-profile-trigger { background: var(--brand-tint); }
.sidenav-profile-avatar {
  inline-size: 40px;
  block-size: 40px;
  border-radius: 50%;
  background: var(--brand-cta);
  color: var(--brand-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fsz-body-sm);  /* 13px initials fit the larger circle */
  font-weight: 700;
  letter-spacing: 0.02em;
  flex-shrink: 0;
  overflow: hidden;
  /* Standard 1px outline ring used by every other avatar circle in
     the app (Student / Examiner cells, live-test cards, profile
     popover, etc.) — defined in tokens.css. Keeps the avatar's edge
     defined when a near-white portrait sits on a near-white sidenav
     surface in light mode. */
  box-shadow: var(--avatar-ring);
}
.sidenav-profile-avatar img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}
.sidenav-profile-avatar-initials { color: inherit; }
.sidenav-profile-text {
  flex: 1 1 auto;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}
.sidenav-profile-name {
  font-size: var(--fsz-body);     /* 14px — primary identity, weight 600 */
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidenav-profile-role {
  font-size: var(--fsz-label);    /* 12px — meta sub-line */
  color: var(--ink-3, var(--ink-2));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidenav-profile-chev {
  flex-shrink: 0;
  color: var(--ink-3, var(--ink-2));
}

/* Profile popover — opens above the trigger row in expanded
   mode (anchored to .sidenav-profile, which has position:
   relative). In rail mode we reposition it to the right of the
   avatar so the 64px rail doesn't squeeze the popover width. */
.sidenav-profile-pop {
  position: absolute;
  inset-block-end: calc(100% + 6px);
  inset-inline-start: 0;
  inset-inline-end: 0;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 6px;
  box-shadow: 0 12px 32px color-mix(in oklab, var(--ink) 18%, transparent);
  z-index: var(--z-dropdown);
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-inline-size: 240px;
}
/* Collapsed-rail override: open to the right of the avatar,
   bottom-aligned with it. (Tablet auto-rail handled below.) */
:root[data-sidenav-collapsed] .sidenav-profile-pop {
  inset-block-end: 0;
  inset-inline-start: calc(100% + 8px);
  inset-inline-end: auto;
  min-inline-size: 240px;
}
@media (min-width: 721px) and (max-width: 1100px) {
  .sidenav-profile-pop {
    inset-block-end: 0;
    inset-inline-start: calc(100% + 8px);
    inset-inline-end: auto;
    min-inline-size: 240px;
  }
}
.sidenav-profile-pop-head {
  padding: 8px 10px 6px;
  border-block-end: 1px solid var(--line);
  margin-block-end: 4px;
}
.sidenav-profile-pop-name {
  font-size: var(--fsz-body-sm);
  font-weight: 600;
  color: var(--ink);
}
.sidenav-profile-pop-email {
  font-size: var(--fsz-label);
  color: var(--ink-3, var(--ink-2));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidenav-profile-pop-item {
  display: flex;
  align-items: center;
  gap: 10px;
  inline-size: 100%;
  min-block-size: 36px;
  padding-block: 6px;
  padding-inline: 10px;
  border: none;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: var(--fsz-body-sm);
  text-align: start;
  text-decoration: none;
  border-radius: 6px;
  cursor: pointer;
  /* Asymmetric hover transition: smooth IN (~80ms fade) but
     INSTANT OUT — the :hover rule defines the in-transition;
     the base rule has none so removing :hover snaps the color
     back immediately. This kills the "trail" of multiple items
     appearing hovered when the cursor sweeps across the rail. */
  transition: background 0ms, color 0ms;
}
.sidenav-profile-pop-item:hover {
  background: color-mix(in oklab, var(--brand-600) 6%, transparent);
  color: var(--ink);
}
.sidenav-profile-pop-item:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }
.sidenav-profile-pop-item.is-destructive { color: var(--err, #b91c1c); }
.sidenav-profile-pop-item.is-destructive:hover {
  background: color-mix(in oklab, var(--err) 8%, transparent);
}
.sidenav-profile-pop-divider {
  block-size: 1px;
  background: var(--line);
  margin-block: 4px;
}
.sidenav-profile-pop-shortcut {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-block: 6px;
  padding-inline: 10px;
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
}
.sidenav-profile-pop-shortcut kbd {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fsz-label);
  padding: 2px 6px;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: 4px;
  color: var(--ink-2);
}

/* ── Profile-menu role switcher (TESTING ONLY) ─────────────────
   QA / demo affordance — lets a reviewer swap the active role
   without going back to the homepage. Sits inside the profile pop
   between the account-settings items and the keyboard-shortcut
   row. Warn-tinted banner + caption note flag it as non-production
   so it can't be mistaken for a real product feature.
   Strip this entire block (CSS + the matching JSX in components.jsx)
   when cutting a production build. */
.sidenav-profile-pop-roleswitch {
  padding: 8px 10px 10px;
  background: color-mix(in oklab, var(--warn) 5%, transparent);
  border-radius: var(--r-sm);
  margin: 2px 4px;
}
.sidenav-profile-roleswitch-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-block-end: 6px;
}
.sidenav-profile-roleswitch-badge {
  /* Warn-tinted pill — signals "non-production" at a glance.
     Caption-tier uppercase letter-spacing so it reads as a tag
     rather than body copy. */
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  background: color-mix(in oklab, var(--warn) 22%, transparent);
  color: color-mix(in oklab, var(--warn) 70%, var(--ink));
  border: 1px solid color-mix(in oklab, var(--warn) 40%, var(--line));
  border-radius: var(--r-pill, 999px);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.sidenav-profile-roleswitch-title {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
}
.sidenav-profile-roleswitch-note {
  font-size: var(--fsz-caption);
  color: var(--ink-3, var(--ink-2));
  line-height: 1.35;
  margin-block-end: 8px;
}
.sidenav-profile-roleswitch-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.sidenav-profile-roleswitch-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 8px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font-size: var(--fsz-label);
  color: var(--ink-2);
  text-align: start;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
  font-family: inherit;
}
.sidenav-profile-roleswitch-item:hover {
  background: var(--bg-raised);
  color: var(--ink);
}
.sidenav-profile-roleswitch-item.is-on {
  /* Active role — brand-tinted bg, brand text, check icon on the
     trailing edge. Click on the current role is a no-op (just
     reloads — harmless), kept clickable for keyboard consistency. */
  background: color-mix(in oklab, var(--brand-500) 10%, transparent);
  color: var(--brand-700, var(--brand-500));
  font-weight: 600;
}
.sidenav-profile-roleswitch-item:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: -2px;
}
.sidenav-profile-roleswitch-item-current {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--brand-700, var(--brand-500));
  flex-shrink: 0;
}

/* ── Backdrop (mobile drawer) ────────────────────────────────── */
.sidenav-backdrop {
  position: fixed;
  inset: 0;
  background: color-mix(in oklab, #000 50%, transparent);
  z-index: calc(var(--z-overlay) - 1);
  animation: sidenav-backdrop-in 180ms var(--sidenav-easing-out);
}
@keyframes sidenav-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ── Main column slot in the sidenav shell ───────────────────── */
/* Two patterns supported:
   1. Single <main> as a direct grid child (home.html)
   2. .app-content wrapper that hosts the page's own layout —
      institute rail + CompactInstituteStrip + main (dashboards,
      saved tests, reports, comingsoon, etc.). Same grid slot in
      both cases; .app-content wraps when the page has more than
      one root-level surface. */
.app--sidenav > .main,
.app--sidenav .main--sidenav,
.app--sidenav > .app-content {
  grid-column: 2;
  grid-row: 1;
  min-inline-size: 0;
}
.app--sidenav > .app-content {
  display: flex;
  flex-direction: column;
}

/* ═══════════════════════════════════════════════════════════════
   RAIL MODE — applied either via tablet width (721–1100) OR via
   the manual collapse attribute on <html>.
   Labels hide, group children get hidden (use flyout instead),
   tooltips on hover (via the title attr on every icon button).
   ═══════════════════════════════════════════════════════════════ */
@media (min-width: 721px) and (max-width: 1100px) {
  /* Tablet default = rail. Overridden below by
     `:root[data-sidenav-expanded]` when the user manually expands
     via the rail-handle. */
  :root:not([data-sidenav-expanded]) .app--sidenav { --sidenav-col: var(--sidenav-w-rail); }
}
/* Manual full override — set when the user clicks the rail-handle
   while in rail-visual mode (desktop rail OR tablet auto-rail).
   `data-sidenav-expanded` is the counterpart to `data-sidenav-collapsed`:
   both attributes are mutually exclusive (React's sync useEffect
   enforces that). At desktop the rule is a no-op (the default grid
   is already full); at tablet it overrides the auto-rail above. */
:root[data-sidenav-expanded] .app--sidenav { --sidenav-col: var(--sidenav-w-full); }
/* ── TABLET (721–1100): expanded menu OVERLAYS, doesn't push ──────
   On tablet the rail is the closed default. When the user opens the
   full menu we float it ABOVE the content as a fixed overlay instead
   of widening the grid column — so the page never reflows. The column
   is pinned at rail width (overriding the manual-full rule above for
   this range only); the sidenav goes fixed + full-width on top, with a
   backdrop the SideNav renders. Desktop (>1100) keeps the push-to-
   expand column; mobile (≤720) keeps its own drawer. */
@media (min-width: 721px) and (max-width: 1100px) {
  :root[data-sidenav-expanded] .app--sidenav { --sidenav-col: var(--sidenav-w-rail); }
  :root[data-sidenav-expanded] .sidenav {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: var(--sidenav-w-full);
    z-index: var(--z-overlay);
    box-shadow: 8px 0 30px color-mix(in oklab, var(--ink) 18%, transparent);
  }
  [dir="rtl"]:root[data-sidenav-expanded] .sidenav {
    inset-inline-start: auto;
    inset-inline-end: 0;
    box-shadow: -8px 0 30px color-mix(in oklab, var(--ink) 18%, transparent);
  }
}
/* Tablet auto-rail rules — duplicated below for the manual
   data-sidenav-collapsed trigger so both modes share the look. */
/* Tablet auto-rail (721–1100). Collapse btn stays absolute
   top-right corner so the user can still click it (or the
   tablet user can move to a >1100 viewport / use ⌘B). Brand
   logo shrinks; wordmark + labels + chevrons hide. */
/* Tablet auto-rail (721–1100). Center the toggle + tenant icon
   horizontally and tighten paddings. Logo HEIGHT stays 40px —
   only the asset swaps to the icon-only variant. */
@media (min-width: 721px) and (max-width: 1100px) {
  /* All tablet rail-visual rules are scoped to
     `:root:not([data-sidenav-expanded])` so they DEACTIVATE when
     the user has manually expanded the rail. In expanded mode, the
     sidenav inherits the desktop full-layout treatment (full logo,
     labels visible, pills horizontal, items left-aligned, group
     children inline). */
  :root:not([data-sidenav-expanded]) .sidenav-brand-mark {
    background-image: var(--logo-icon-light, url('assets/tenants/pl/PL-icon-light.svg'));
    background-size: contain;
    background-position: center;
  }
  [data-theme="dark"]:root:not([data-sidenav-expanded]) .sidenav-brand-mark {
    background-image: var(--logo-icon-dark, url('assets/tenants/pl/PL-icon-dark.svg'));
  }
  /* Same recipe as the desktop rail block: labels + group chevrons fade
     (scoped to .sidenav so the portaled flyout is untouched); footer
     controls stay display:none; header/content/items keep their expanded
     geometry; brand drops its inline padding so the icon-only mark fills the
     rail width and isn't clipped. Profile trigger + footer stay at expanded
     padding so the avatar doesn't jump. */
  :root:not([data-sidenav-expanded]) .sidenav .sidenav-item-label,
  :root:not([data-sidenav-expanded]) .sidenav .sidenav-group-chev {
    opacity: 0;
    max-inline-size: 0;
    margin: 0;
    visibility: hidden;
    pointer-events: none;
  }
  :root:not([data-sidenav-expanded]) .lang-pill,
  :root:not([data-sidenav-expanded]) .sidenav-profile-text,
  :root:not([data-sidenav-expanded]) .sidenav-profile-chev { display: none; }
  :root:not([data-sidenav-expanded]) .sidenav-notif-dot { margin-inline-start: 0; position: absolute; inset-block-start: 8px; inset-inline-end: 14px; }
  :root:not([data-sidenav-expanded]) .sidenav-group-children { display: none; }
}
/* Manual rail — same visual treatment as the tablet auto-rail
   block above. Logo height stays at the 40px default so it
   matches the expanded state. Scoped to ≥721px so it CANNOT
   leak into the mobile drawer: at ≤720px the sidenav is a
   drawer (full-layout-when-open, hidden-when-closed) — the
   rail-collapsed flag is a desktop/tablet concept only, and
   the user's prior desktop choice must not change how the
   mobile drawer renders. (Without this scope, opening the
   burger from a tablet/desktop session that had collapsed the
   rail would show: icon-only logo, missing chevrons / lang
   label / profile chev, center-aligned item rows — none of
   which match the standard expanded menu the drawer is meant
   to be.) */
@media (min-width: 721px) {
  /* Collapsed: swap to the icon-only asset and use `background-size: contain`
     (not the base auto-40px) so the icon fits the rail mark on BOTH axes for
     ANY aspect ratio — a non-square icon scales to whichever edge hits first
     instead of overflowing the rail width. Centered in the rail. */
  :root[data-sidenav-collapsed] .sidenav-brand-mark {
    background-image: var(--logo-icon-light, url('assets/tenants/pl/PL-icon-light.svg'));
    background-size: contain;
    background-position: center;
  }
  [data-theme="dark"]:root[data-sidenav-collapsed] .sidenav-brand-mark {
    background-image: var(--logo-icon-dark, url('assets/tenants/pl/PL-icon-dark.svg'));
  }
  /* Labels + group chevrons FADE + collapse their inline-size (smooth)
     instead of display:none (instant cut). Scoped to inside .sidenav so the
     portaled group-flyout labels (rendered in document.body) stay visible. */
  :root[data-sidenav-collapsed] .sidenav .sidenav-item-label,
  :root[data-sidenav-collapsed] .sidenav .sidenav-group-chev {
    opacity: 0;
    max-inline-size: 0;
    margin: 0;
    visibility: hidden;
    pointer-events: none;
  }
  /* Footer controls keep display:none — their flex slots would add phantom
     gap that shoves the avatar off-center (mirrors iVP). */
  :root[data-sidenav-collapsed] .lang-pill,
  :root[data-sidenav-collapsed] .sidenav-profile-text,
  :root[data-sidenav-collapsed] .sidenav-profile-chev { display: none; }
  /* Items / group heads / notif KEEP their expanded padding (NO re-center)
     so every icon holds the exact same x-position as the rail opens/closes
     — and doesn't drift sideways while its label collapses. */
  :root[data-sidenav-collapsed] .sidenav-notif-dot {
    margin-inline-start: 0;
    position: absolute;
    inset-block-start: 8px;
    inset-inline-end: 14px;
  }
  :root[data-sidenav-collapsed] .sidenav-group-children { display: none; }
  /* Profile trigger keeps its expanded geometry (NOT re-centered) and the
     footer keeps its 10px padding, so the 40px avatar holds the exact same
     x-position open↔closed — no sideways jump. */
}

/* BUG FIX: items INSIDE a portaled flyout must always render
   their icon + label like the expanded sidenav row, even when
   the rail is collapsed. Without this, the generic
   `[data-sidenav-collapsed] .sidenav-item-label { display: none }`
   above cascades into the flyout and you see icons-only — making
   the flyout useless. The flyout lives in document.body, so it
   inherits the html-level rule unless we override it here. */
:root[data-sidenav-collapsed] .sidenav-flyout .sidenav-item-label,
:root[data-sidenav-collapsed] .sidenav-flyout .sidenav-item { display: flex; }
:root[data-sidenav-collapsed] .sidenav-flyout .sidenav-item {
  justify-content: flex-start;
  padding-inline-start: 10px;
}
@media (min-width: 721px) and (max-width: 1100px) {
  .sidenav-flyout .sidenav-item-label,
  .sidenav-flyout .sidenav-item { display: flex; }
  .sidenav-flyout .sidenav-item {
    justify-content: flex-start;
    padding-inline-start: 10px;
  }
}

/* ═══════════════════════════════════════════════════════════════
   GROUP FLYOUT — portaled into document.body. Shown when a
   collapsed-rail group icon is hovered/focused/tapped.
   ═══════════════════════════════════════════════════════════════ */
.sidenav-flyout {
  position: fixed;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 16px 40px color-mix(in oklab, var(--ink) 22%, transparent);
  padding: 8px;
  min-inline-size: 200px;
  z-index: var(--z-tooltip);
  /* No fade-in animation — instant appearance avoids any
     overlap or trail when sweeping cursor across group icons. */
}
/* Invisible "hover bridge" extending leftward from the flyout
   into the 8px gap between trigger and panel. Hovering this
   area keeps the parent flyout's mouseenter state active, so
   the cursor can traverse the visual gap slowly without
   triggering close. RTL extends rightward. */
.sidenav-flyout::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: -16px;
  inline-size: 16px;
}
[dir="rtl"] .sidenav-flyout::before {
  inset-inline-start: auto;
  inset-inline-end: -16px;
}
@keyframes sidenav-flyout-in {
  from { opacity: 0; transform: translateX(-4px); }
  to   { opacity: 1; transform: translateX(0); }
}
[dir="rtl"] .sidenav-flyout {
  animation-name: sidenav-flyout-in-rtl;
}
@keyframes sidenav-flyout-in-rtl {
  from { opacity: 0; transform: translateX(4px); }
  to   { opacity: 1; transform: translateX(0); }
}
.sidenav-flyout-head {
  padding-block: 4px;
  padding-inline: 8px;
  font-size: var(--fsz-caption);
  font-weight: 700;
  color: var(--ink-2);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-block-end: 4px;
}
.sidenav-flyout-body { display: flex; flex-direction: column; gap: 2px; }
/* Inside the flyout, child items render as regular sidenav-items
   (the SideNav passes them through). Reset the inline indent so
   they don't get shifted in the floating panel. */
.sidenav-flyout .sidenav-item.is-child {
  /* Children inside the rail-mode flyout match top-level
     sizing — same font, same icon, no extra indent inside the
     panel (the panel itself provides the visual hierarchy). */
  padding-inline-start: 10px;
}

/* ═══════════════════════════════════════════════════════════════
   MOBILE — ≤768: single column, sidenav becomes drawer, mobile
   top strip becomes visible.
   ═══════════════════════════════════════════════════════════════ */
.mobile-top-strip { display: none; }   /* desktop default */

@media (max-width: 720px) {
  .app--sidenav {
    grid-template-columns: 1fr !important;
    grid-template-rows: var(--mobile-strip-h) 1fr;
  }
  .mobile-top-strip {
    display: flex;
    align-items: center;
    gap: 8px;
    grid-row: 1;
    grid-column: 1;
    position: sticky;
    inset-block-start: 0;
    z-index: var(--z-sticky);
    block-size: var(--mobile-strip-h);
    padding-inline: 12px;
    background: var(--bg-raised);
    border-block-end: 1px solid var(--line);
  }
  .mts-burger {
    inline-size: 40px;
    block-size: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: transparent;
    color: var(--ink);
    cursor: pointer;
  }
  .mts-burger:hover { background: var(--brand-tint); }
  .mts-burger:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
  .mts-brand {
    display: flex;
    align-items: center;
    text-decoration: none;
    color: var(--ink);
    min-inline-size: 0;
    block-size: 36px;
    padding-inline: 4px;
  }
  /* Mobile top strip uses the full logo (wordmark included)
     since the strip is horizontal and has space. Aspect ratio
     ~3.24:1, sized to fit a 36px-tall strip. */
  .mts-brand-mark {
    display: block;
    block-size: 28px;
    inline-size: 116px;          /* ~28 × 3.24 ≈ 91; bumped to 116 to read the wordmark */
    /* Tenant-driven via --logo-tenant-* (set by boot.js), PL wordmark fallback. */
    background-image: var(--logo-tenant-light, url('assets/tenants/pl/PL-logo-light.svg'));
    background-repeat: no-repeat;
    background-position: left center;   /* hug the start edge — narrow logos (EDC 70px) no longer float in the 116px box */
    background-size: contain;
    flex-shrink: 0;
  }
  [data-theme="dark"] .mts-brand-mark {
    background-image: var(--logo-tenant-dark, url('assets/tenants/pl/PL-logo-dark.svg'));
  }
  /* RTL: the strip mirrors (burger on the right), so the logo hugs the
     inline-start = right edge of its box. */
  [dir="rtl"] .mts-brand-mark { background-position: right center; }
  .mts-spacer { flex: 1 1 auto; }
  /* Mobile-top-strip notification bell — keep the bell glyph
     neutral (ink) at all times, even when there's an unread
     notification. The red dot beside it carries the "alert"
     signal; tinting the bell brand-colored on mobile competes
     with the dot and reads as a state change the user didn't
     trigger. Desktop keeps the brand-tinted bell (defined
     above by `.icon-btn--has-notif { color: var(--brand-600) }`)
     because the topnav has more breathing room and the tint
     is a useful "look here" cue. */
  .mobile-top-strip .icon-btn,
  .mobile-top-strip .icon-btn--has-notif,
  .mobile-top-strip .icon-btn:hover { color: var(--ink); }
  /* Up-size the bell glyph + alert dot for touch & glanceability.
     Bell: 15px → 20px (matches the sidenav drawer's bell). Dot:
     7px → 10px and reposition to the bell's top-right with a
     thicker bg ring so it stays legible against the brand-tint
     hover background.
     ──
     The Icon component (components.jsx) sets the SVG's width
     and height via INLINE STYLE (`style="width: 15px; …"`), not
     HTML width/height attributes. Inline style beats any
     external CSS rule except `!important`, so this override
     uses `!important` deliberately — it's not propping up a
     specificity bug, it's the only way to override an inline
     style applied by a shared component without forking it. */
  .mobile-top-strip .icon-btn > svg {
    width: 20px !important;
    height: 20px !important;
  }
  .mobile-top-strip .dot-indicator {
    inline-size: 10px;
    block-size: 10px;
    inset-block-start: 4px;
    inset-inline-end: 4px;
    border-width: 2px;
  }
  .app--sidenav > .main,
  .app--sidenav .main--sidenav,
  .app--sidenav > .app-content { grid-row: 2; grid-column: 1; }
  .sidenav {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: min(86vw, var(--sidenav-w-full));
    grid-row: auto;
    block-size: 100dvh;
    transform: translateX(-100%);
    transition: transform 240ms var(--sidenav-easing-out);
    z-index: var(--z-overlay);
    border-inline-end: 1px solid var(--line);
    box-shadow: 8px 0 30px color-mix(in oklab, var(--ink) 18%, transparent);
  }
  [dir="rtl"] .sidenav {
    /* In RTL the drawer should slide in from the VISUAL RIGHT (the
       reading-start edge), to match desktop sidenav placement and
       Arabic UX convention. `inset-inline-start: 0` resolves to
       `right: 0` in RTL, anchoring the drawer's right edge to the
       viewport's right edge. `translateX(100%)` then moves it 100%
       of its own width to the right — OFF the right edge — for the
       hidden state.
       The previous override flipped to `inset-inline-end: 0` (= left
       edge in RTL) and tried to hide via `translateX(100%)`; at
       viewports wider than 2× the drawer width, the "hidden"
       position landed in the middle of the screen instead of
       off-screen — visible bug at narrow widths (e.g. 711px). */
    inset-inline-start: 0;
    inset-inline-end: auto;
    transform: translateX(100%);
    border-inline-end: 0;
    border-inline-start: 1px solid var(--line);
    box-shadow: -8px 0 30px color-mix(in oklab, var(--ink) 18%, transparent);
  }
  .sidenav.is-mobile-open { transform: translateX(0); }
  /* Mobile drawer ignores the rail-collapsed attr — always full. */
  :root[data-sidenav-collapsed] .sidenav { inline-size: min(86vw, var(--sidenav-w-full)); }
  :root[data-sidenav-collapsed] .sidenav-item-label,
  :root[data-sidenav-collapsed] .sidenav-notif-btn .sidenav-item-label,
  :root[data-sidenav-collapsed] .sidenav-profile-text { display: flex; }
  :root[data-sidenav-collapsed] .sidenav-group-children { display: flex; }
  /* On mobile the sidenav is a drawer — opened via the hamburger in
     the mobile-top-strip, dismissed via the backdrop. The desktop
     edge-handle (which toggles rail ↔ full) has no meaning here: the
     drawer is always full when open, fully hidden when closed. Hide
     it so it doesn't sit as a stray sliver against the screen edge
     and so it doesn't contradict the burger menu. */
  .sidenav-rail-handle { display: none; }
}

/* Pre-React skeleton on the new chrome — chrome itself static, no
   shimmer; body skeletons inside each page still shimmer. */
.sidenav--placeholder, .mobile-top-strip--placeholder { pointer-events: none; }
.sidenav--placeholder .skel-row {
  display: block;
  block-size: 40px;
  margin-block: 4px;
  margin-inline: 10px;
  border-radius: 8px;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}

/* Reduced-motion: snap all chrome transitions. */
@media (prefers-reduced-motion: reduce) {
  .app--sidenav,
  .sidenav,
  .sidenav-group-chev,
  .sidenav-group-children,
  .sidenav-flyout,
  .sidenav-backdrop,
  .skip-link { transition: none !important; animation: none !important; }
}

/* ═══════════════════════════════════════════════════════════════
   LIVE TESTS PAGE  (livetests.html)
   Reuses iVE primitives wherever possible (.table-card, .data-table,
   .filter-search, .btn, .result-pill, .row-photo, LiveTestCard). Only
   the unique surfaces below carry `.ltp-*` classes:
     - Branch picker chip (page-header subtitle)
     - Workspace split layout (queue + live zone)
     - Pre-start card (compact action-oriented variant of LiveTestCard)
     - Examiner controls strip below each ongoing LiveTestCard
     - Inline AssignPanel + vehicle picker grid
     - Status pill (queue-row state: Arrived / Planned)
     - Empty banner + toast
   ═══════════════════════════════════════════════════════════════ */

/* ── Pre-React skeleton ─────────────────────────────────────── */
.lt2-skeleton-workspace {
  display: grid;
  grid-template-columns: minmax(440px, 0.6fr) 1fr;
  gap: var(--s-5);
}
@media (max-width: 1100px) {
  .lt2-skeleton-workspace { grid-template-columns: 1fr; }
}

/* ── Branch picker chip (in PageHeader subtitle) ────────────── */
.ltp-branch-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.ltp-branch-chip:hover,
.ltp-branch-chip.is-open {
  background: var(--brand-tint);
  border-color: color-mix(in oklab, var(--brand-500) 30%, var(--line));
  color: var(--brand-700);
}
.ltp-branch-chip svg { opacity: 0.7; }
.ltp-branch-chip.is-locked {
  cursor: default;
  color: var(--ink-2);
  background: var(--bg-sunken);
}
.ltp-branch-chip.is-locked:hover {
  background: var(--bg-sunken);
  border-color: var(--line);
  color: var(--ink-2);
}
.ltp-branch-pop {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md);
  min-inline-size: 220px;
  max-inline-size: 280px;
  padding: var(--s-1);
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: var(--z-dropdown);
}
.ltp-branch-pop-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  font: var(--fs-body-sm);
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  transition: background var(--t-fast), color var(--t-fast);
}
.ltp-branch-pop-item:hover {
  background: var(--brand-tint);
  color: var(--brand-700);
}
.ltp-branch-pop-item.is-active {
  background: var(--brand-tint-2);
  color: var(--brand-700);
  font-weight: 600;
}
.ltp-branch-pop-item > span:first-of-type { flex: 1; }
.ltp-branch-pop-item > svg { color: var(--ink-3); flex-shrink: 0; }
.ltp-branch-pop-hq {
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--brand-700);
  background: var(--brand-tint);
  padding: 1px 6px;
  border-radius: var(--r-pill);
  margin-inline-start: auto;
}

/* ── Workspace split ─────────────────────────────────────────── */
.ltp-workspace {
  display: grid;
  grid-template-columns: minmax(440px, 0.6fr) 1fr;
  gap: var(--s-5);
  margin-block-start: var(--s-4);
  align-items: flex-start;
}
.ltp-workspace[data-mode="stack"] { grid-template-columns: 1fr; }
@media (max-width: 1180px) {
  .ltp-workspace { grid-template-columns: 1fr; }
}

/* ── Empty banner ────────────────────────────────────────────── */
.ltp-empty-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--brand-tint);
  border: 1px solid color-mix(in oklab, var(--brand-500) 18%, var(--line));
  border-radius: var(--r-lg);
  margin-block-start: var(--s-4);
}
.ltp-empty-banner-icon {
  flex: 0 0 auto;
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--bg-raised);
  color: var(--brand-700);
  box-shadow: var(--sh-xs);
}
.ltp-empty-banner-text { display: flex; flex-direction: column; gap: 2px; font: var(--fs-body-sm); color: var(--ink-2); }
.ltp-empty-banner-text strong { color: var(--ink); font: var(--fs-body-strong); letter-spacing: var(--ls-body-strong); }

/* ── Queue card (uses .table-card chrome + .data-table inside) ─ */
.ltp-queue.table-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.ltp-queue-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-block-end: 1px solid var(--line);
  flex-wrap: wrap;
}
.ltp-queue-titles { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.ltp-queue-title {
  margin: 0;
  font: var(--fs-h3);
  letter-spacing: var(--ls-h3);
  color: var(--ink);
}
.ltp-queue-sub {
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--ink-3);
}
.ltp-queue-search.filter-search {
  flex: 1 1 240px;
  max-inline-size: 320px;
}

/* Section header inside the queue (Pending / Completed) */
.ltp-section-head {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  inline-size: 100%;
  text-align: start;
  /* Sentence-case h3-size title so the section reads as a real
     heading, not a thead label. Brand stripe on the inline-start
     edge anchors the section visually. */
  padding: var(--s-3) var(--s-4);
  padding-inline-start: calc(var(--s-4) + 6px);
  background: var(--bg-sunken);
  border: 0;
  border-block-start: 1px solid var(--line);
  border-block-end: 1px solid var(--line);
  font: var(--fs-h3);
  letter-spacing: var(--ls-h3);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--t-fast);
  /* Overflow-hidden lets the head's rounded corners clip the
     brand stripe so the stripe's top hugs the curve where the
     section meets the table above (it stays square on non-
     first sections that already have flat top corners). */
  overflow: hidden;
}
.ltp-section-head::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 3px;
  background: var(--brand-600);
  pointer-events: none;
}
.ltp-section-head:hover { background: color-mix(in oklab, var(--brand-500) 5%, var(--bg-sunken)); }
.ltp-section-head svg { transition: transform var(--t-fast); color: var(--ink-3); }
.ltp-section-head.is-collapsed svg { transform: rotate(-90deg); }
[dir="rtl"] .ltp-section-head.is-collapsed svg { transform: rotate(90deg); }
.ltp-section-count {
  color: var(--ink-3);
  font: var(--fs-body-sm);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}

/* yardlive — each section is a standalone bordered block:
   rounded head-top + rounded table-bottom, with a small breathing
   gap between adjacent sections. The brand stripe on the head
   defaults to --brand-600; `[data-stripe="neutral"]` switches it
   to a quiet ink tone (used for historical / less-actionable
   sections like Completed). */
/* First section sits flush with the parent's 12px flex gap above
   it — matching the filter→table gap on Saved Tests. The previous
   `var(--s-4)` (16px) top margin stacked with `.main`'s gap to
   total 28px, which read as too much breathing room between the
   filters and the data area. */
.ylt-section { margin-block-start: 0; }

/* ── Yard Live Tests — 2-column split below the filter bar ─────
   Right column = Live canvas. Left column = Pending + Completed
   tables stacked. Target ratio at wide viewports: 40/60 (tables /
   canvas).
   The LEFT column has a 660px floor — the Pending table's
   natural width (≈631px) plus a small breathing margin. Below
   that the tables would either clip the trailing columns (Status,
   Type, Actions / Assign) or trigger internal horizontal scroll,
   both bad. So as the viewport narrows, the LEFT column stays at
   660px and the RIGHT canvas absorbs the loss until it hits its
   own breakpoint (≤1100px) and the layout stacks.
   Mobile (≤1100px) stacks vertically: Live canvas comes FIRST so
   the timely / actionable surface is what the user sees when they
   scroll past the filter bar. */
.ylt-split {
  display: grid;
  /* Priority-shrink layout (per user spec): tables column claims its
     natural width FIRST (`--ylt-left-floor`, default 622px = sum of
     visible column widths + card chrome), capped at that value. The
     right canvas takes everything that's left via 1fr, with a hard
     minimum of 406px — derived from: list-view strip floor (380px,
     see `.d2-live-rows .lt-h1` / `.ylc-grid--list .lt-h1`) + grid
     padding (12+12=24) + section border (1+1=2). Below 406 the
     strip would overflow the canvas horizontally, which clipped
     the trailing action cluster and the chevron. Was 340 (cards-
     mode minimum); strips are wider than the LiveTestCard floor,
     so the canvas floor had to lift accordingly.
     Grid track-sizing algorithm:
       1. Maximize non-fr tracks within their max → tables grows from
          0 up to 622 first.
       2. Distribute leftover free space to fr tracks → canvas
          absorbs anything past tables' 622.
     Net behavior as the viewport narrows:
       · Wide:        tables=622, canvas absorbs (>>340)
       · Medium:      tables=622, canvas shrinks toward its 340 floor
       · Canvas-min:  tables=622, canvas=340
       · Beyond that: canvas pinned at 340, tables compresses below 622
       · Compressed:  table content overflows `.table-scroll` →
                      existing `.is-overflowing` ResizeObserver kicks
                      horizontal scroll IN the tables column. Canvas
                      itself never horizontally scrolls — it just
                      stops at the 340 floor.
     `--ylt-left-floor` is still set inline by the page; when the user
     hides toggleable columns, the value drops accordingly, freeing
     pixels for the canvas rather than spreading them across the
     remaining columns. */
  grid-template-columns:
    minmax(0, var(--ylt-left-floor, 622px))
    minmax(406px, 1fr);
  /* Gap tightened 16 → 12 to recover a few pixels for the columns
     and the live canvas. */
  gap: 12px;
  align-items: start;
}
.ylt-split-tables {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-inline-size: 0;
}
.ylt-split-canvas {
  min-inline-size: 0;
  /* Cap the canvas element at 480 — the SAME max as the cards
     inside. The grid track for canvas is 1fr (absorbs surplus),
     so at wide viewports the track may be much wider; the canvas
     element itself stops at 480 and the leftover sits as empty
     page background past it. This lets tables prioritize keeping
     its 622 floor — the canvas doesn't gobble surplus, so tables
     stays at preferred width and doesn't shrink unless the
     viewport truly can't accommodate it. */
  max-inline-size: 480px;
}

/* ── Independent column scroll (tablet + desktop, ≥721px) ───────
   The page itself stops being the scroll context; instead, each
   split column owns its own vertical scroll. To make the split
   absorb the right amount of vertical space without a magic
   "subtract this many px from the viewport" offset, we put `<main>`
   into a 100dvh flex column, let the page-header take its natural
   height, and let `.ylt-split` flex-grow into whatever's left.
   `:has()` scopes the `<main>` override so only pages with a
   `.ylt-split` adopt this layout — other pages keep their natural
   flow. */
@media (min-width: 721px) {
  main.main:has(> .ylt-split) {
    block-size: 100dvh;
    display: flex;
    flex-direction: column;
    min-block-size: 0;
  }
  .ylt-split {
    /* Was `calc(100dvh - <static-offset>)` which undercounted the
       real chrome height (page-header + main padding) by ~22px,
       making the page scroll vertically when it shouldn't. flex:1
       inside the main column auto-adapts to any chrome height. */
    flex: 1;
    min-block-size: 0;
    overflow: hidden;
    /* Override `align-items: start` from the base so the column
       tracks stretch to the full container height — otherwise grid
       items size to content and the inner overflow never triggers. */
    align-items: stretch;
  }
  .ylt-split-tables,
  .ylt-split-canvas {
    /* `min-block-size: 0` is required inside grid for `overflow-y`
       to clip — without it the column grows to fit content. */
    overflow-y: auto;
    min-block-size: 0;
    /* Small bottom inset so the last row doesn't sit flush against
       the scroll edge. */
    padding-block-end: 12px;
  }
  /* Canvas section sizing — two goals at once:
     (1) When card content is SHORT (e.g. idle status panel ~900px),
         the section should still fill the splitCanvas column so its
         "stage" tone (oklch(0.88) light / oklch(0.20) dark) extends
         down to the column floor; otherwise leftover column space
         reads as blank page background below the section.
     (2) When card content is TALL (6 cards in 1-up = ~1700px) and
         overflows the splitCanvas's visible viewport (triggering its
         own scroll), the section must GROW to fit the content so
         the stage tone keeps backing the cards as the user scrolls
         past the visible viewport — otherwise the section bg ends
         at the visible viewport edge and cards beyond appear to
         sit on the page background ("outside the canvas" bug).

     The previous `flex: 1; min-block-size: 0` rule satisfied (1)
     but broke (2) — flex:1 SIZES the section to the column's
     visible main-axis space and prevents it from growing with
     content. Replaced with `flex: none; min-block-size: 100%`:
     - flex: none → section sizes to content (no flex-grow cap).
     - min-block-size: 100% → enforces a floor of the parent's
       block-size (= splitCanvas content height), so the section
       still fills the column when content is short.
     Net: section is `max(content_height, column_height)`. */
  .ylt-split-canvas {
    display: flex;
    flex-direction: column;
  }
  .ylt-split-canvas > .ylc-section {
    flex: none;
    min-block-size: 100%;
  }
}

/* The medium-narrow media query (721-1440) that used to force
   `1fr` for tables + capped canvas track at 480 has been removed.
   It made the canvas section bigger than necessary in the empty
   state — tables shrank to absorb the surplus instead of canvas
   shrinking toward its own minimum. The base rule with the new
   `max-inline-size: 480px` on `.ylt-split-canvas` handles this
   correctly: tables prioritizes its 622 floor, canvas grows up
   to 480 (the cap on the element, even when the track is wider). */

/* Wide viewports (≥ 1100px, canonical breakpoint) — at this point
   the base rule had tables stuck at its 622 max and canvas capped
   at 480, which left a strip of dead space inside the right column
   (the grid track grew with the surplus, but `.ylt-split-canvas`
   clamped at 480 so 100-200px of empty space appeared between the
   canvas's right edge and the track's right edge). Lifting the cap
   here lets the canvas absorb that surplus — single-card states
   stretch the card width, multi-card states pack into 2-up sooner.
   Threshold was previously 1400 which gated the relief band too
   high; 1100 matches the canonical iVE breakpoint scheme. */
@media (min-width: 1100px) {
  /* `.ylt-split` grid-template-columns override removed — the base
     rule (`minmax(0, var(--ylt-left-floor, 622px)) minmax(340px, 1fr)`)
     applies at ≥1100 too. This caps tables at 622 and lets canvas
     absorb ALL surplus via 1fr, so 2-up engages as soon as canvas
     has room for 2 × 316 card-min + 12 gap = 644 inner
     (~668 track). With tables at 622 + 668 + 12 gap = 1302 split
     inner, 2-up kicks in at viewport ≈ 1400 (collapsed sidenav).

     The earlier override used `minmax(var(--ylt-left-floor), 1fr)`
     on tables (both tracks 1fr), which made tables grow alongside
     canvas at wide viewports — splitting surplus equally. That
     pushed the 2-up threshold to viewport ≥ 1700, since canvas
     only got half the extra space. The user prefers 2-up engaging
     as early as possible, so we hand all surplus to the canvas. */
  .ylt-split-canvas {
    /* Lift the 480 cap. Cards inside still cap at 480 each, so
       2-up engages naturally when canvas inner exceeds ~644
       (= 2 × 316 card-min + 12 gap). */
    max-inline-size: none;
  }
}

/* ── Sticky heads inside the column-scroll model ───────────────
   In the new layout the page itself no longer scrolls (≥721px);
   the left and right columns own their own vertical scroll. That
   changes sticky's reference container from the document to each
   column. The base `.data-table thead th` rule uses `top: 40px`
   (compensating for the legacy page-level compact-strip), and
   `.ylc-head` had no sticky at all. Both need scoped overrides
   inside the split so the heads pin to the top of their column
   as the user scrolls. */
@media (min-width: 721px) {
  /* Section head (Pending tests / Completed tests) — sticks first
     at the column top. Each head is bounded by its parent
     `.ylt-section`, so sticky cascades naturally: section-1's head
     stays pinned until section-1 scrolls past, then section-2's
     head takes over. `z-index: 6` keeps it above the table thead
     (z:4) so the thead docks UNDER it as it scrolls up. */
  .ylt-split-tables .ylt-section > .ltp-section-head {
    position: sticky;
    top: 0;
    z-index: 6;
  }
  /* Table head — pins BELOW the section head. The base `.data-table
     thead th` rule uses `top: 40px` (legacy compensation for the
     page-level compact-strip). Inside the column-scroll model the
     thead docks under the section head at `top: 46px` (section
     head intrinsic height). */
  .ylt-split-tables .data-table thead th {
    top: 46px;
  }
  /* Canvas head — pin the Cards / List toggle + live-count pill
     to the top of `.ylt-split-canvas` as it scrolls. `z-index: 5`
     keeps it above the cards but below portal'd menus (kebab,
     tooltips). The head's `background: var(--bg-raised)` is
     already opaque, so cards scrolling beneath are properly
     covered. */
  .ylt-split-canvas .ylc-head {
    position: sticky;
    top: 0;
    z-index: 5;
  }
}

/* ── Mobile (≤720px) ────────────────────────────────────────────
   Stack vertically. Canvas on top so the timely / actionable
   surface is the first thing the user sees when scrolling past
   the filter bar. Order swaps source-order via `order` so DOM
   semantics stay tables-then-canvas (accessibility tree). Page
   scrolls naturally — no fixed-height container, no internal
   scroll — feels native for thumb scroll. */
@media (max-width: 720px) {
  .ylt-split {
    grid-template-columns: 1fr;
    block-size: auto;
    overflow: visible;
    align-items: start;
  }
  .ylt-split-tables,
  .ylt-split-canvas {
    overflow-y: visible;
    min-block-size: auto;
    padding-block-end: 0;
  }
  .ylt-split-canvas {
    order: 0;
    /* Lift the 480 cap in mobile-stack mode — both columns are
       full-width when stacked, so capping canvas at 480 leaves an
       asymmetric look (tables full, canvas narrower). Let canvas
       fill the column like tables does. */
    max-inline-size: none;
  }
  .ylt-split-tables  { order: 1; }
}

/* ── Live canvas (.ylc-*) ──────────────────────────────────────
   A discrete focal panel — noticeably darker than the page so
   white cards inside POP against it. The previous --bg-muted was
   too close to --bg-raised; we now use a deeper neutral that's
   clearly differentiated. Cards inside use a strong line color so
   their borders stay visible against the dark backdrop. */
.ylc-section {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  /* Bumped 0.93 → 0.88 — the previous lightness was only ~7% darker
     than the page bg and the canvas didn't read as a discrete focal
     stage. 0.88 = ~12% darker, enough that the white cards inside
     visibly POP and the canvas reads as a deliberate surface separate
     from the tables column on the other side. */
  background: oklch(0.88 0.006 250);
  /* Two-layer shadow: the standard outer xs lift PLUS a 1px inset
     line on the top edge. The inset reads as "the cards inside are
     floating ABOVE this surface" — subtle stage-floor cue without
     adding visual weight. */
  box-shadow:
    var(--sh-xs),
    inset 0 1px 0 color-mix(in oklab, var(--ink) 6%, transparent);
  display: flex;
  flex-direction: column;
  min-block-size: 200px;
}
[data-theme="dark"] .ylc-section {
  /* Bumped 0.18 → 0.20 in dark mode for the same reason as light —
     keeps the canvas one notch deeper than the surrounding page bg
     so the cards inside read as floating elements on a darker stage. */
  background: oklch(0.20 0.006 250);
  box-shadow:
    var(--sh-xs),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
.ylc-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 14px;
  border-block-end: 1px solid var(--line);
  background: var(--bg-raised);
  border-start-start-radius: var(--r-lg);
  border-start-end-radius: var(--r-lg);
}
.ylc-head-title { display: flex; align-items: center; gap: 8px; min-inline-size: 0; }
.ylc-pill {
  /* Live-count pill — mirrors the d2-live-pill from dashboards. */
  font-size: var(--fsz-caption);
}
.ylc-head-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
.ylc-empty {
  padding: 40px 16px;
  text-align: center;
  color: var(--ink-3);
  font-size: var(--fsz-body);
  line-height: 1.5;
}

/* ── Yard status panel (canvas empty state) ───────────────────
   Renders inside `.ylc-section` when zero live tests are running.
   Replaces the prior plain "no live tests" line with an operationally
   useful overview: hero strip, vehicle breakdown (3 statuses),
   compact stats (examiners / queue / last test), primary "Start
   next test" CTA. See `YardStatusPanel` in yardlive.html. */
.ylc-status-panel {
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* Hero strip — brand-tinted block with a "P" parking glyph mark
   on the leading edge + headline + subtitle. Communicates "idle"
   without reading as "broken / empty". */
.ylc-status-hero {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  background: color-mix(in oklab, var(--brand-500) 6%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand-500) 16%, var(--line));
  border-radius: var(--r-md);
}
.ylc-status-hero-mark {
  inline-size: 44px;
  block-size: 44px;
  border-radius: 10px;
  background: color-mix(in oklab, var(--brand-500) 14%, var(--bg-raised));
  color: var(--brand-700, var(--brand-500));
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 22px;
  line-height: 1;
  flex-shrink: 0;
  letter-spacing: -0.04em;
}
.ylc-status-hero-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-inline-size: 0;
}
.ylc-status-hero-text strong {
  font-size: var(--fsz-h3);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
}
.ylc-status-hero-text span {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.4;
}
/* Vehicle breakdown card — total + stacked bar + numbered legend
   + future-facing "Vehicle status →" deep-link CTA. */
.ylc-status-vehicles {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ylc-status-vehicles-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}
.ylc-status-section-title {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
}
/* Single-fact summary in the head — "X of Y available". Replaces
   the previous 3-state breakdown (ready / attention / offline +
   "of N"). The detailed states still surface on each per-vehicle
   chip below, so this number is purely the at-a-glance roll-up:
   how many vehicles are usable right now. */
.ylc-status-section-summary {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: var(--fsz-label);
  color: var(--ink-2);
}
.ylc-status-section-summary b {
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.ylc-status-section-summary-of,
.ylc-status-section-summary-tail {
  color: var(--ink-3);
  font-size: var(--fsz-caption);
}
.ylc-status-section-summary-tail {
  margin-inline-start: 2px;
  text-transform: lowercase;
  letter-spacing: 0;
}
[dir="rtl"] .ylc-status-section-summary-tail { text-transform: none; }
/* Status dot — 8px circle, three semantic tones (ok / warn /
   inkish-gray for offline). Used in the head's inline counts AND
   inside the chip grid below. */
.ylc-status-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
}
.ylc-status-dot.is-ready     { background: var(--ok, oklch(0.65 0.16 145)); }
.ylc-status-dot.is-attention { background: var(--warn, oklch(0.75 0.16 70)); }
.ylc-status-dot.is-offline   { background: color-mix(in oklab, var(--ink) 32%, transparent); }
.ylc-status-dot.is-in-use    { background: var(--info, oklch(0.65 0.16 240)); }
/* In-use card variant — borrowed info-tinted treatment so it
   doesn't compete with the warn (attention) and err (offline)
   alarm colors. Vehicles in use are healthy, just busy. */
.ylc-status-vh.is-in-use {
  border-color: color-mix(in oklab, var(--info, oklch(0.65 0.16 240)) 28%, var(--line));
  background: color-mix(in oklab, var(--info, oklch(0.65 0.16 240)) 3%, var(--bg-raised));
}
/* Per-vehicle card grid — replaces the previous chip layout. Each
   card carries explicit status TEXT pills (no color-only coding):
   "Online", "Offline", "Ready", "Not ready". Color is supporting
   information, not the only signal — passes contrast and reads
   accurately for color-blind users.
   Layout: auto-fit grid with min track 140px → 1-2 cards per row
   at the canvas's narrow widths, 3+ at wider canvas widths. */
.ylc-status-vh-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px;
}
.ylc-status-vh {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 10px 10px 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-inline-size: 0;
  position: relative;
}
.ylc-status-vh.is-attention { border-color: color-mix(in oklab, var(--warn, oklch(0.75 0.16 70)) 40%, var(--line)); }
.ylc-status-vh.is-offline   { background: color-mix(in oklab, var(--ink) 3%, var(--bg-raised)); }
/* Head row: vehicle code + transmission badge + optional warning
   button (when status !== 'ready'). */
.ylc-status-vh-head {
  display: flex;
  align-items: center;
  gap: 6px;
}
.ylc-status-vh-code {
  font-weight: 700;
  color: var(--ink);
  font-size: var(--fsz-h3);
  letter-spacing: -0.01em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  flex: 1;
  min-inline-size: 0;
}
/* Transmission badge — small "A" or "M" letter mark. Reads as
   "automatic" / "manual" at a glance. Tinted by transmission so
   the eye can group cards by type. */
.ylc-status-vh-transm {
  display: inline-grid;
  place-items: center;
  inline-size: 20px;
  block-size: 20px;
  border-radius: var(--r-sm);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  flex-shrink: 0;
}
.ylc-status-vh-transm.is-auto {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  color: var(--brand-700, var(--brand-500));
}
.ylc-status-vh-transm.is-manual {
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  color: var(--ink);
}
/* Warning button — circular alert affordance with the design
   system's canonical RIPPLE animation. Same keyframes shape as
   `.pill-dot.has-pulse` (currentColor halo radiates outward, then
   fades), just sized for a 22px button instead of a 6px dot.
   Color = warn (attention) or err (offline). Click opens the
   portaled issue popover. Hover scale + open state's solid ring
   are both unified with the system's interactive button language. */
.ylc-status-vh-warn {
  display: inline-grid;
  place-items: center;
  inline-size: 22px;
  block-size: 22px;
  border-radius: 50%;
  border: 0;
  background: var(--warn);
  color: white;
  cursor: pointer;
  flex-shrink: 0;
  padding: 0;
  /* Canonical ripple (matches `.pill-dot.has-pulse` shape) — halo
     starts at the button edge, expands outward to 10px while
     fading, then snaps back. Currency-of-color is the bg so the
     halo inherits the same warn/err tone. */
  animation: ylc-status-warn-ripple 1.6s ease-out infinite;
  transition: transform 120ms ease, background 120ms ease;
}
.ylc-status-vh-warn.is-offline { background: var(--err); }
.ylc-status-vh-warn:hover { transform: scale(1.08); }
.ylc-status-vh-warn.is-open {
  /* Once the popover is open, the ripple stops — replaced with a
     steady ring that anchors the visual connection between the
     trigger and its open menu. */
  animation: none;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--warn) 28%, transparent);
}
.ylc-status-vh-warn.is-offline.is-open {
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--err) 28%, transparent);
}
@keyframes ylc-status-warn-ripple {
  0%   { box-shadow: 0 0 0 0    currentColor; }
  70%  { box-shadow: 0 0 0 10px transparent; }
  100% { box-shadow: 0 0 0 0    transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .ylc-status-vh-warn { animation: none; }
}
/* Pills row — exactly ONE design-system `.pill` per vehicle. The
   actual pill rules (sizing, tone variants, border, etc.) live in
   the shared `.pill` block elsewhere in this file — nothing
   bespoke here, just the flex layout that wraps multiple pills
   if a future variant ever needs more than one. */
.ylc-status-vh-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

/* ── Vehicle issue popover (portaled to document.body) ─────────
   Appears next to the clicked warning button on a non-ready
   vehicle card. Shows the specific subsystems with problems +
   two action CTAs. Lives in document.body via React portal so
   it escapes the panel's overflow/clip context. */
.ylc-vh-popover {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md, 0 6px 20px color-mix(in oklab, var(--ink) 14%, transparent));
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ylc-vh-popover.is-attention { border-color: color-mix(in oklab, var(--warn, oklch(0.75 0.16 70)) 36%, var(--line)); }
.ylc-vh-popover.is-offline   { border-color: color-mix(in oklab, var(--err, oklch(0.6 0.2 25)) 36%, var(--line)); }
.ylc-vh-popover-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-label);
}
.ylc-vh-popover.is-attention .ylc-vh-popover-head { color: color-mix(in oklab, var(--warn, oklch(0.75 0.16 70)) 70%, var(--ink)); }
.ylc-vh-popover.is-offline .ylc-vh-popover-head   { color: color-mix(in oklab, var(--err, oklch(0.6 0.2 25)) 70%, var(--ink)); }
.ylc-vh-popover-head strong {
  font-weight: 700;
  color: var(--ink);
}
.ylc-vh-popover-code {
  margin-inline-start: auto;
  font-size: var(--fsz-caption);
  background: var(--bg-sunken);
  color: var(--ink-2);
  padding: 1px 6px;
  border-radius: var(--r-sm);
  font-weight: 700;
}
.ylc-vh-popover-issues {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ylc-vh-popover-issues li {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 8px;
  background: var(--bg-sunken);
  border-radius: var(--r-sm);
}
.ylc-vh-popover-issue-label {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
}
.ylc-vh-popover-issue-detail {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  line-height: 1.35;
}
/* CTAs (Log maintenance / Open in vehicle management) removed for
   now — to be wired alongside the vehicle-management module. The
   popover stays purely informational: header + issue list. */

/* ── Assign-vehicle modal ───────────────────────────────────────
   Portaled to document.body so it sits above all chrome. Backdrop
   absorbs clicks to dismiss; modal body stops propagation so
   interior clicks don't close. The modal is moderately tall —
   header + student-context + 2 vehicle sections + language picker
   + footer — so it's vertically scrollable when content exceeds
   viewport height. */
.ylc-modal-backdrop {
  position: fixed;
  inset: 0;
  background: color-mix(in oklab, var(--ink) 38%, transparent);
  backdrop-filter: blur(2px);
  z-index: 1000;
  display: grid;
  place-items: center;
  padding: 24px;
  animation: ylc-modal-fade-in 160ms ease-out;
}
@keyframes ylc-modal-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.ylc-modal {
  background: var(--bg-raised);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-lg, 0 16px 48px color-mix(in oklab, var(--ink) 24%, transparent));
  max-block-size: calc(100dvh - 48px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.ylc-modal--assign {
  inline-size: min(620px, 100%);
}
/* Student header — mirrors AssignedCard's `.ylc-assign-head`
   layout grammar: avatar + identity (bilingual stacked) + attempt
   block. Used in the modal as the "who you're assigning" context
   above the maneuver list and vehicle picker. Background uses the
   same brand-tinted treatment as the profile-popover header — gray
   bg felt institutional / outdated. */
.ylc-modal-student {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 16px;
  /* White body beneath the tinted header band — the header tint only
     reads as a "band" if the section under it is neutral. (Was a 4%
     brand wash that doubled up with the header and muddied it.) */
  background: var(--bg-raised);
  border-bottom: 1px solid var(--line);
}
.ylc-modal-student-head {
  display: flex;
  align-items: center;
  gap: 12px;
  /* `flex-wrap` so the test-language block on the trailing edge
     drops below the identity column at narrow widths instead of
     squeezing the name. */
  flex-wrap: wrap;
}
.ylc-modal-student-avatar {
  inline-size: 44px;
  block-size: 44px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  color: white;
  font-weight: 700;
  font-size: var(--fsz-label);
  letter-spacing: 0.04em;
}
.ylc-modal-student-avatar img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
.ylc-modal-student-identity {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-inline-size: 0;
}
.ylc-modal-student-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ylc-modal-student-sub {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.3;
  /* Apply the project's Arabic font (IBM Plex Sans Arabic) so AR
     glyphs render with proper shapes (same treatment as the
     profile-popover sub-name). Left-aligned regardless of page
     direction. */
  font-family: var(--font-ar);
  text-align: start;
}
.ylc-modal-student-attempt {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  flex-shrink: 0;
  gap: 1px;
}
.ylc-modal-student-attempt-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.ylc-modal-student-attempt-num {
  font-size: var(--fsz-h2);
  font-weight: 700;
  color: var(--ink);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* Quick-id row — inline traffic-file + attempt pair, sits directly
   under the names. Both are identifiers, both belong with the
   student name cluster (replaces the previous "own field with
   hairline divider" treatment which felt visually fragmented). */
.ylc-modal-student-ids {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  margin-block-start: 4px;
}
.ylc-modal-student-id-item {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
}
.ylc-modal-student-id-label {
  color: var(--ink-3);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.ylc-modal-student-id-val {
  color: var(--ink);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ylc-modal-student-id-sep {
  color: var(--ink-3);
  font-weight: 600;
}
/* Test-language picker — top-right of the student head row.
   Label CAP above the toggle (stacked column). In LTR, `flex-end`
   pushes label + toggle to the right (cross-axis end). In RTL the
   same `flex-end` would push them to the LEFT, which makes the
   label sit on the wrong side of its block. Explicit RTL override
   flips to `flex-start` so the label still reads "right-aligned
   above the toggle" in Arabic mode. */
.ylc-modal-student-lang {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  flex-shrink: 0;
}
[dir="rtl"] .ylc-modal-student-lang {
  align-items: flex-start;
}
.ylc-modal-student-lang-label {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1;
}
/* Maneuvers — sits at the bottom of the student card. Intro copy
   reads as "this specific candidate's maneuvers" so it doesn't look
   like a generic info dump. */
.ylc-modal-student-maneuvers {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-block-start: 10px;
  border-block-start: 1px dashed color-mix(in oklab, var(--ink) 10%, transparent);
}
.ylc-modal-student-maneuvers-intro {
  font-size: var(--fsz-caption);
  color: var(--ink-2);
  font-style: italic;
}
/* Section-level help text — sits under a section header, explains
   what the operator should do next. Used under "Available vehicles"
   to give the action prompt adjacent to the picker. */
.ylc-modal-section-help {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.4;
  margin-block-end: 4px;
}
/* Collapsible "Show N unavailable" affordance — compact text link
   by default; chevron flips on open. Mirrors the design-system
   chev-rotate pattern used elsewhere. */
.ylc-modal-unavail-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  cursor: pointer;
  align-self: flex-start;
  transition: background var(--t-fast), color var(--t-fast);
}
.ylc-modal-unavail-toggle:hover {
  background: var(--bg-sunken);
  color: var(--ink);
}
.ylc-modal-unavail-toggle svg {
  transition: transform var(--t-fast);
}
.ylc-modal-unavail-toggle.is-open svg {
  transform: rotate(180deg);
}
/* Maneuvers row — pills matching the assigned-card maneuver chips
   (.ylc-assign-maneuver-pill): subtle brand-tint, name only. The
   numeric badge was dropped — the pill order already conveys the
   sequence, and the numbers risked reading as attempt counts. */
.ylc-modal-mans {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.ylc-modal-man {
  display: inline-flex;
  align-items: center;
  padding-block: 2px;
  padding-inline: 10px;
  background: color-mix(in oklab, var(--brand-500) 8%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  border-radius: var(--r-pill, 999px);
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  line-height: 1.5;
}
.ylc-modal-head {
  /* Title + close X on one row. Subtitle was removed (was a near-
     duplicate of the help text under the available-vehicles
     section) — the help text adjacent to the picker does the
     same job better. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  /* Brand-tinted header band, matching .modal-head (tokens.css
     --modal-head-*). Destructive confirms add .is-danger (red). */
  background: var(--modal-head-bg);
  border-bottom: 1px solid var(--modal-head-line);
  border-start-start-radius: var(--r-lg);
  border-start-end-radius: var(--r-lg);
}
.ylc-modal-head.is-danger  { background: var(--modal-head-bg-danger);  border-bottom-color: var(--modal-head-line-danger); }
.ylc-modal-head.is-warn    { background: var(--modal-head-bg-warn);    border-bottom-color: var(--modal-head-line-warn); }
.ylc-modal-head.is-success { background: var(--modal-head-bg-success); border-bottom-color: var(--modal-head-line-success); }
.ylc-modal-head strong {
  font-size: var(--fsz-h3);
  font-weight: 700;
  color: var(--ink);
  line-height: 1.2;
}
.ylc-modal-close {
  /* sm icon-only tier — modal scale. See tokens.css `--cta-h-*`. */
  inline-size: var(--cta-h-sm);
  block-size: var(--cta-h-sm);
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--ink-3);
  cursor: pointer;
  flex-shrink: 0;
}
.ylc-modal-close:hover { background: var(--bg-sunken); color: var(--ink); }
/* Student-context chip strip — quick "you're assigning X" header. */
.ylc-modal-context {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 16px;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--line);
}
.ylc-modal-context-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: var(--fsz-label);
}
.ylc-modal-context-label {
  color: var(--ink-3);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: var(--fsz-caption);
}
.ylc-modal-context-val {
  color: var(--ink);
  font-weight: 600;
}
.ylc-modal-section {
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  border-bottom: 1px solid var(--line);
}
.ylc-modal-section:last-of-type { border-bottom: 0; }
.ylc-modal-section-head {
  /* Title + count snapped together at the leading edge (not pushed
     to opposite edges). The count chip sits IMMEDIATELY after the
     title — reads as one phrase ("Available vehicles 6") rather
     than as two disconnected pieces of metadata. */
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 8px;
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.ylc-modal-section-count {
  font-weight: 700;
  color: var(--ink);
  /* Subtle count chip — gives the number a contained visual weight
     so it reads as a quantity tag rather than just trailing text. */
  background: color-mix(in oklab, var(--ink) 6%, transparent);
  padding: 1px 6px;
  border-radius: var(--r-pill, 999px);
  min-inline-size: 20px;
  text-align: center;
}
/* Vehicle grid inside the modal — slightly tighter than the yard
   status panel grid since the modal's width is fixed (≤560). */
.ylc-modal-vh-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px;
}
.ylc-modal-vh {
  /* 2-row card: row 1 = vehicle code + transmission badge, row 2 =
     status pill. More substantial than the chip variant on the
     status panel because the modal context calls for a "select me"
     affordance, not just an at-a-glance status indicator. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding: 10px 12px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
  font-family: inherit;
  text-align: start;
  min-block-size: 80px;
}
.ylc-modal-vh-row1 {
  display: flex;
  align-items: center;
  gap: 8px;
}
.ylc-modal-vh-row2 {
  display: flex;
  align-items: center;
}
button.ylc-modal-vh {
  /* Override the user-agent button bg/font when the element is a button. */
  font: inherit;
  color: var(--ink);
}
.ylc-modal-vh.is-ready:hover:not(.is-selected) {
  border-color: color-mix(in oklab, var(--brand-500) 40%, var(--line));
  background: color-mix(in oklab, var(--brand-500) 4%, var(--bg-raised));
}
.ylc-modal-vh.is-selected {
  border-color: var(--brand-500);
  background: color-mix(in oklab, var(--brand-500) 10%, var(--bg-raised));
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-500) 18%, transparent);
}
.ylc-modal-vh.is-disabled {
  cursor: not-allowed;
  opacity: 0.65;
  background: var(--bg-sunken);
}
.ylc-modal-vh-code {
  font-weight: 700;
  font-size: var(--fsz-h3);
  color: var(--ink);
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  flex: 1;
}
.ylc-modal-vh-transm {
  display: inline-grid;
  place-items: center;
  /* Bumped 20→22 to fit a 12px M/A letter (was 11px) without the
     glyph hugging the badge edge — keeps the design-system
     `--fsz-label` minimum on the transmission marker. */
  inline-size: 22px;
  block-size: 22px;
  border-radius: var(--r-sm);
  font-size: var(--fsz-label);
  font-weight: 700;
  flex-shrink: 0;
}
.ylc-modal-vh-transm.is-auto {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  color: var(--brand-700, var(--brand-500));
}
.ylc-modal-vh-transm.is-manual {
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  color: var(--ink);
}
/* No-vehicles empty state — soft warn-tinted card with a clear
   "why" + "what to do" message. */
.ylc-modal-empty {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px;
  background: color-mix(in oklab, var(--warn, oklch(0.75 0.16 70)) 6%, var(--bg-raised));
  border: 1px dashed color-mix(in oklab, var(--warn, oklch(0.75 0.16 70)) 30%, var(--line));
  border-radius: var(--r-md);
}
.ylc-modal-empty strong {
  font-size: var(--fsz-body);
  color: var(--ink);
  font-weight: 600;
}
.ylc-modal-empty span {
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.45;
}
/* `.ylc-modal-lang-row` was the bottom-of-modal test-language row;
   the toggle moved up into the student section
   (`.ylc-modal-student-lang`). Block removed. */
/* Footer — trailing-aligned action row. */
.ylc-modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 16px;
  background: var(--bg-sunken);
  border-top: 1px solid var(--line);
}
.ylc-modal-foot .btn-primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ── ConfirmActionModal ─────────────────────────────────────────
   Two-stage modal for reason-gated destructive workflows on Live
   Test cards (LT-BP-003 family). The base `.ylc-modal` chrome is
   shared with AssignVehicleModal; these rules add the
   reason-modal-specific sizing + form-field treatments + summary
   surface for the confirm step. */
.ylc-modal--confirm {
  inline-size: min(520px, calc(100dvw - 48px));
  max-block-size: calc(100dvh - 48px);
}
/* Modal body padding — the base `.ylc-modal-body` element has no
   padding rule by default. Symmetric top/bottom + extra room
   between the last content block and the footer separator so the
   layout doesn't read as cramped at the bottom. */
.ylc-modal--confirm .ylc-modal-body {
  padding: 16px 20px 20px 20px;
}
.ylc-modal--confirm .ylc-modal-head {
  padding: 14px 16px 14px 20px;
  /* Border comes from the base .ylc-modal-head (tinted) + tone class
     (.is-danger for Stop/Disqualify) — don't re-declare it here or it
     would override the tint. */
  display: flex;
  align-items: center;
  gap: 8px;
}
.ylc-modal--confirm .ylc-modal-title {
  flex: 1;
  min-inline-size: 0;
  margin: 0;
  font-size: var(--fsz-h3, 16px);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
}
.ylc-modal--confirm .ylc-modal-foot {
  padding: 12px 20px;
}
/* Student identity strip — pinned at the top of the modal body
   on both stages so the operator never loses track of WHICH test
   they're acting on. Avatar + primary name + opposite-script sub
   + vehicle chip on the trailing edge. */
.ylc-modal-student-strip {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  background: color-mix(in srgb, var(--brand-500) 4%, var(--bg-raised));
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  margin-block-end: 14px;
}
.ylc-modal-student-strip-avatar {
  inline-size: 40px;
  block-size: 40px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--bg-sunken);
  display: inline-grid;
  place-items: center;
  flex-shrink: 0;
  box-shadow: var(--avatar-ring);
}
.ylc-modal-student-strip-avatar img {
  inline-size: 100%; block-size: 100%; object-fit: cover;
}
.ylc-modal-student-strip-initials {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink-2);
}
.ylc-modal-student-strip-text {
  flex: 1;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.ylc-modal-student-strip-name {
  font-size: var(--fsz-body);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ylc-modal-student-strip-sub {
  font-size: var(--fsz-caption);
  font-weight: 400;
  color: var(--ink-3);
  font-family: var(--font-ar);
  line-height: 1.3;
}
[dir="ltr"] .ylc-modal-student-strip-sub { text-align: left; }
[dir="rtl"] .ylc-modal-student-strip-sub { text-align: right; }
.ylc-modal-student-strip-veh {
  flex-shrink: 0;
}
.ylc-modal-title {
  margin: 0;
  font-size: var(--fsz-h3, 16px);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
}
.ylc-modal-subtitle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fsz-caption);
  color: var(--ink-3);
}
.ylc-modal-subtitle-sep { color: var(--ink-4, var(--ink-3)); }
.ylc-modal-back {
  /* sm icon-only tier — modal scale. See tokens.css `--cta-h-*`. */
  display: grid;
  place-items: center;
  inline-size: var(--cta-h-sm);
  block-size: var(--cta-h-sm);
  border: 0;
  background: transparent;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  margin-inline-end: 4px;
}
.ylc-modal-back:hover { background: var(--bg-sunken); color: var(--ink); }
.ylc-modal-explain {
  margin: 0 0 12px 0;
  font-size: var(--fsz-label);
  color: var(--ink-2);
  line-height: 1.5;
}
/* Form field shared by reason select + notes/evidence textareas. */
.ylc-modal-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-block-end: 12px;
}
.ylc-modal-field-label {
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
}
.ylc-modal-field-req { color: var(--err); margin-inline-start: 2px; }
.ylc-modal-field-opt { color: var(--ink-3); font-weight: 400; }
.ylc-modal-select,
.ylc-modal-textarea {
  inline-size: 100%;
  font-family: inherit;
  font-size: var(--fsz-label);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 8px 10px;
  box-shadow: inset 0 0 0 0 transparent;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.ylc-modal-textarea {
  resize: vertical;
  min-block-size: 60px;
  line-height: 1.5;
}
.ylc-modal-select:focus,
.ylc-modal-textarea:focus {
  outline: none;
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-500) 18%, transparent);
}
/* Summary surface (stage 'confirm') — read-back of the user's
   inputs before the destructive Confirm click. */
.ylc-modal-summary {
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-block-end: 8px;
}
.ylc-modal-summary-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ylc-modal-summary-key {
  font-size: var(--fsz-caption);
  font-weight: 600;
  color: var(--ink-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.ylc-modal-summary-val {
  font-size: var(--fsz-label);
  color: var(--ink);
  line-height: 1.4;
  white-space: pre-wrap;
}
.ylc-modal-summary-warning {
  margin: 4px 0 0 0;
  font-size: var(--fsz-label);
  color: var(--err);
  font-weight: 500;
}
.ylc-modal-confirm-destructive {
  /* Final destructive confirm button — same shape as btn-primary
     but tinted red to reinforce that this is the irreversible
     action. Hover/focus follow the same brand-button conventions
     (slightly deeper red on hover). */
  background: var(--err) !important;
  border-color: var(--err) !important;
  color: #fff !important;
}
.ylc-modal-confirm-destructive:hover {
  background: color-mix(in oklab, var(--err) 88%, black) !important;
}
.ylc-modal-confirm-destructive:focus-visible {
  outline: 2px solid color-mix(in oklab, var(--err) 60%, transparent);
  outline-offset: 2px;
}

/* Outcome picker — radio list shown as the first gate inside the
   unified Stop modal. Each option is a stacked label+sub block so
   the operator can read what each outcome means without resolving
   ambiguous labels. Selected option gets a brand-tinted border +
   bg so the choice reads at a glance. */
.ylc-modal-outcome-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ylc-modal-outcome {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--bg-raised);
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease;
}
.ylc-modal-outcome:hover {
  border-color: color-mix(in oklab, var(--brand-500) 40%, var(--line));
  background: color-mix(in oklab, var(--brand-500) 4%, var(--bg-raised));
}
.ylc-modal-outcome.is-selected {
  border-color: var(--brand-500);
  background: color-mix(in oklab, var(--brand-500) 8%, var(--bg-raised));
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand-500) 14%, transparent);
}
.ylc-modal-outcome input[type="radio"] {
  margin-block-start: 3px;
  flex-shrink: 0;
  accent-color: var(--brand-500);
}
.ylc-modal-outcome-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-inline-size: 0;
}
.ylc-modal-outcome-label {
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--ink);
}
.ylc-modal-outcome-sub {
  font-size: var(--fsz-caption);
  color: var(--ink-3);
  line-height: 1.4;
}

/* Consequence warning box — prominent attention block on stage 2
   of the modal. Warn-tone surface + leading-edge accent bar; the
   icon is now a simple inline glyph in the warn color (was a
   filled circle wrapper that read as visually off-balance with
   the triangle inside). Body content is a bullet list so the
   consequences scan in a glance instead of buried in a wall of
   prose. */
.ylc-modal-consequence {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 14px 16px;
  background: color-mix(in oklab, var(--warn) 10%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--warn) 38%, var(--line));
  border-radius: var(--r-sm);
  border-inline-start-width: 4px;
  border-inline-start-color: var(--warn);
  margin-block-start: 14px;
}
.ylc-modal-consequence-icon {
  flex-shrink: 0;
  display: inline-grid;
  place-items: center;
  inline-size: 22px;
  block-size: 22px;
  color: color-mix(in oklab, var(--warn) 80%, var(--ink));
  margin-block-start: 1px;
}
.ylc-modal-consequence-icon svg { stroke-width: 2 ; }
.ylc-modal-consequence-text {
  flex: 1;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ylc-modal-consequence-title {
  font-size: var(--fsz-label);
  font-weight: 700;
  color: color-mix(in oklab, var(--warn) 70%, var(--ink));
  letter-spacing: 0.02em;
}
/* Bullet list — compact, scannable. Custom marker (•) in the
   warn-tone so it reads as a deliberate list rather than the
   browser's grey default disc. */
.ylc-modal-consequence-bullets {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* Inline info bullets — used by single-stage confirms (e.g.
   Return-to-waitlist). Plain bullet list with no surrounding
   warn-tone box; the action is reversible / non-destructive so
   the ceremonial "Before you confirm" warning doesn't apply.
   Marker dot uses muted ink rather than err so it doesn't read
   as a warning. */
.ylc-modal-info-bullets {
  margin: 0 0 4px 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.ylc-modal-info-bullets li {
  position: relative;
  padding-inline-start: 16px;
  font-size: var(--fsz-label);
  color: var(--ink);
  line-height: 1.4;
}
.ylc-modal-info-bullets li::before {
  content: '';
  position: absolute;
  inset-inline-start: 4px;
  inset-block-start: 0.55em;
  inline-size: 4px;
  block-size: 4px;
  border-radius: 50%;
  background: var(--ink-3);
}
.ylc-modal-consequence-bullets li {
  position: relative;
  padding-inline-start: 16px;
  font-size: var(--fsz-label);
  color: var(--ink);
  line-height: 1.4;
}
.ylc-modal-consequence-bullets li::before {
  content: '';
  position: absolute;
  inset-inline-start: 4px;
  inset-block-start: 0.55em;
  inline-size: 4px;
  block-size: 4px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--warn) 70%, var(--ink-3));
}
.ylc-modal-consequence-irrev {
  margin: 4px 0 0 0;
  font-size: var(--fsz-label);
  font-weight: 600;
  color: var(--err);
}

/* Cascading more-menu — back arrow + submenu chev for the Stop
   sub-action navigation inside `.ylc-more-menu`. */
.ylc-more-item--has-submenu {
  /* Right-side chevron should sit at the trailing edge while the
     leading icon + label cluster occupies the rest of the row. */
  justify-content: flex-start;
}
.ylc-more-item--has-submenu > svg:last-child {
  margin-inline-start: auto;
  color: var(--ink-3);
}
.ylc-more-item--back {
  font-weight: 600;
  color: var(--ink-2);
  border-block-end: 1px solid var(--line);
  margin-block-end: 4px;
  padding-block-end: 8px;
}
.ylc-more-item--back:hover { color: var(--ink); }
/* RTL directional flip — the car icon points right by default
   (hood/front on its right side). In RTL the icon naturally moves
   to the right of the label via flex flow, but the icon itself
   still points right — which now reads as "pointing away from the
   text," visually awkward. Mirror it horizontally so it points
   toward the label, consistent with the rest of the RTL layout.
   Covers both the AssignVehicleModal's confirm-button car icon
   AND every row-level Assign button in the queue tables (those
   use `.ylt-assign-btn`). */
[dir="rtl"] .ylc-modal-foot .btn-primary svg,
[dir="rtl"] .ylt-assign-btn svg {
  transform: scaleX(-1);
}

/* Check-in CTA — text-link style (no filled bg, no border) that
   mirrors `.ylc-ongoing-canvas-btn` (the "View test" link on the
   ongoing card). Visual contrast against the solid-brand Assign
   button: pending rows show a quiet text-link; once attended,
   the row swaps to the full primary Assign CTA. The clipboardList
   icon trails the label to reinforce "take attendance" semantics. */
.ylt-checkin-btn {
  /* L2 text-link CTA — see `.ylc-assign-return` for the family. */
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  min-block-size: var(--cta-h-md);
  border: 0;
  background: transparent;
  color: var(--brand-700, var(--brand-500));
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 600;
  cursor: pointer;
  border-radius: var(--r-sm, 6px);
  flex: 0 0 auto;
  white-space: nowrap;
}
.ylt-checkin-btn:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.ylt-checkin-btn:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
.ylt-checkin-btn svg { flex-shrink: 0; color: currentColor; }

/* Off-window time warning — appears in the Check-in modal when
   the appointment is more than 30 minutes from "now". Informational
   note in brand/info tone (blue, NOT warn-orange) so it doesn't
   read identical to the destructive "Before you confirm" warning
   box that sits next to it. Clock icon reinforces "this is about
   timing", not "this is dangerous". Non-blocking — the operator
   can still proceed. */
.ylc-modal-time-warn {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  background: color-mix(in oklab, var(--brand-500) 8%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand-500) 32%, var(--line));
  border-radius: var(--r-sm);
  border-inline-start-width: 4px;
  border-inline-start-color: var(--brand-500);
  margin-block-end: 12px;
  font-size: var(--fsz-label);
  color: var(--ink);
  line-height: 1.45;
}
.ylc-modal-time-warn svg {
  color: var(--brand-600, var(--brand-500));
  flex-shrink: 0;
  margin-block-start: 2px;
}

/* Typed-to-confirm gate — extra-friction text input that appears
   on stage 2 of the modal when the chosen outcome requires the
   operator to type a specific word (e.g. "absent") before the
   destructive Confirm button enables. Positioned after the
   consequence box so the operator reads what'll happen first,
   then types to acknowledge. The required word is rendered as a
   small mono pill (uppercase, err-tone bg) so it's unmissable
   inside the label sentence. */
.ylc-modal-typed-confirm {
  margin-block-start: 14px;
}
.ylc-modal-typed-word {
  display: inline-block;
  padding: 1px 8px;
  margin-inline: 4px;
  background: color-mix(in oklab, var(--err) 14%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--err) 38%, var(--line));
  border-radius: 4px;
  font-family: var(--font-mono);
  /* 1em = `--fsz-label` (12px LTR / 13px RTL) — matches the
     surrounding label sentence and meets the design-system
     minimum. Was 0.92em (~11px); bumped to align with the
     12px-floor rule shared across modal chrome. */
  font-size: 1em;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--err);
  vertical-align: baseline;
}
.ylc-modal-input {
  inline-size: 100%;
  font-family: inherit;
  font-size: var(--fsz-label);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 8px 10px;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.ylc-modal-input:focus {
  outline: none;
  border-color: var(--err);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--err) 18%, transparent);
}
.ylc-modal-confirm-destructive:disabled {
  background: color-mix(in oklab, var(--err) 50%, var(--bg-sunken)) !important;
  border-color: transparent !important;
  cursor: not-allowed;
  opacity: 0.5;
}
/* Vehicle status CTA — brand text-link, trailing chev. Pinned
   to the trailing edge of the card via flex (auto-margin
   alternative would also work; flex keeps it simpler). */
.ylc-status-vehicle-cta {
  /* L2 text-link CTA — see `.ylc-assign-return` for the family. */
  align-self: flex-end;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  min-block-size: var(--cta-h-md);
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--brand-700, var(--brand-500));
  font-size: var(--fsz-label);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.ylc-status-vehicle-cta:hover {
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
  color: var(--brand-800, var(--brand-700));
}
/* `.ylc-status-stats` and its children removed by request — the
   examiners-on-shift / in-queue / last-test stat strip is being
   reworked into a different surface, so the rules and supporting
   children classes were deleted here to avoid orphan CSS. */

/* The "Start next test" CTA that used to live at the bottom of
   `.ylc-status-panel` was removed — the actual assignment flow lives
   in the queue table's per-row Assign buttons, so a panel-level
   primary CTA would either duplicate that or auto-pick a candidate
   (which operators usually want to do themselves). Panel is now
   purely informational. */

.ylc-grid {
  padding: 12px;
  display: grid;
  gap: 12px;
}
.ylc-grid--cards {
  /* `auto-fit` + `minmax(316px, 1fr)` for the TRACKS,
     plus a per-item `max-inline-size: 480px` for the CARDS — the
     two-knob design lets the grid pack as many cards as fit at
     the MIN (316) while the cards individually cap at 480.
       · 1 card alone → only one surviving track (auto-fit
         collapses empties), the 1fr stretches the surviving
         track to the full canvas width, BUT the card inside
         is capped at 480 via max-inline-size. Result: card sits
         at 480 with the rest of the canvas as empty space —
         "breathing room" beyond the 2-up width without ballooning.
       · 2 cards → 2 tracks of (canvas − gap)/2 each. If that's
         under 480, cards fill their tracks; if over, they cap
         at 480.
       · N cards → N tracks, each capped per the same rule.

     Track sizing for the 2-up threshold uses the MIN (316), not
     the max — so 2-up engages whenever the canvas can hold two
     cards at their smallest legible size, with cards shrinking
     to fit. (An earlier draft used `minmax(316, 480)` which made
     track count use the MAX, requiring canvas ≥ 996 for 2-up —
     too restrictive.)

     Bare `316px` is intentional (NOT `min(316px, 100%)`): the
     `min(X, 100%)` idiom is sometimes used to let the column
     floor follow the container down when the container is
     narrower than X. But `min()` inside `auto-fit minmax()`
     creates a circular dependency — the `100%` resolves against
     the container width, so the resolved track min ends up
     equal to the container width, and auto-fit packs only ONE
     column even when there's room for two. The split layout's
     canvas track has a hard 340px minimum (= 316 card + 24 grid
     padding), so the canvas inner is always ≥ 316; horizontal
     scroll never appears in practice.

     Why 316 floor: action CTAs ("Engage brake" / "Release brake")
     + status pill truncate below this width across EN + AR.
     Why 480 ceiling: ~50% wider than the floor — enough breathing
     room for a single card without wasting interior space. */
  grid-template-columns: repeat(auto-fit, minmax(316px, 1fr));
}
.ylc-grid--cards > * {
  /* Cards fill their grid track (default `justify-self: stretch`).
     The earlier `max-inline-size: 480` cap meant single cards in
     1-up mode (canvas inner 480–643px, just below the 2-up
     threshold of 644) sat at 480 with ~150px of empty leftover
     track on the inline-end side — that empty strip surfaced the
     canvas section background as a "phantom" grey rectangle next
     to the card in RTL. Removing the cap lets cards stretch to
     fill the full track width; in practice the track caps out at
     ~643px in 1-up mode (just below where 2-up engages) and at
     ~half-canvas-width in 2-up, so cards never get pathologically
     wide. */
}
.ylc-grid--list {
  /* Compact view — multi-column strips (same 380px floor as the
     dashboard's .d2-live-rows). auto-fit packs 2-3+ strips across the
     canvas instead of one full-width row per test. */
  grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
}
/* Phones (≤480px): the 380px strip floor is wider than the viewport, so
   the list view overflowed horizontally with no escape hatch. Below 480
   we go single-column and release the floor so each strip shrinks to fit;
   the strip's own ≤720 / ≤420 container layout (3-row + wrapped pill,
   already defined in live-test-card.css) then reflows it cleanly. This is
   a VIEWPORT media query (not a container query) so desktop strip sizing
   is untouched — it does NOT reintroduce the desktop size-flip the 380
   floor was originally added to remove. */
@media (max-width: 480px) {
  .ylc-grid--list { grid-template-columns: 1fr; }
  .ylc-grid--list .lt-h1 { min-inline-size: 0; }
}

/* ── List-view density tweaks ──────────────────────────────────
   In list mode we hide the secondary identity furniture (attempt
   + examiner stack) so each row scans faster. Action buttons keep
   their full text labels (icon-only was tested and reverted —
   irreversible actions read better with explicit copy). */
.ylc-grid--list .lt-h1-meta {
  display: none;
}

/* ── Ongoing strip (list-view) ─────────────────────────────────
   Same 3-col × 2-row grid as the assigned strip, with different
   trailing actions:
     row 1: avatar | identity-wrap(1fr)  | View test (text link)
     row 2: veh-chip + timer + pill ──── | kebab (Brake/Stop/Disqualify)
   Avatar lives only on row 1, top-aligned with the English name.
   Status cluster spans the full row on row 2 with justify-self:
   start so it doesn't dictate col 3's width (kebab sizes col 3). */
.lt-h1.ylc-strip-ongoing {
  grid-template-columns: auto 1fr auto;
  grid-template-rows: auto auto;
  grid-template-areas: none;
  /* `row-gap: 0` (not 8) — the 8px gap between row 1 (identity)
     and row 2 (status + buttons) is now applied via explicit
     `margin-block-start: 8px` on the row-2 items (see the rule
     in the d2-live-grid neighbourhood block above). Keeping
     row-gap on the grid would re-apply 8px between EVERY pair
     of rows including the trailing 0-height segments row,
     adding a phantom 8px band at the strip's collapsed bottom. */
  row-gap: 0;
  padding-block: 10px;
}
.lt-h1.ylc-strip-ongoing .lt-h1-avatar {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
}
.lt-h1.ylc-strip-ongoing .lt-h1-identity-wrap {
  grid-column: 2;
  grid-row: 1;
}
.lt-h1.ylc-strip-ongoing .ylc-strip-view-cta {
  /* Mirrors `.ylc-ongoing-canvas-btn` from cards-view — text-only
     brand link, no chrome, with the external-link icon trailing.
     Top-aligned so it sits beside the English name (row-1 first line)
     rather than floating vertically centered. */
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: start;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  border: 0;
  background: transparent;
  color: var(--brand-700, var(--brand-500));
  text-decoration: none;
  font-size: var(--fsz-label);
  font-weight: 600;
  border-radius: var(--r-sm, 6px);
}
.lt-h1.ylc-strip-ongoing .ylc-strip-view-cta:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.lt-h1.ylc-strip-ongoing .ylc-strip-status-row {
  /* Spans the full row with justify-self: start so the cluster
     anchors left without dictating col 3's track width. */
  grid-column: 1 / -1;
  grid-row: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  justify-self: start;
  align-self: center;
  min-inline-size: 0;
}
.lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn,
.lt-h1.ylc-strip-ongoing .ylc-action--more {
  /* Both action buttons share col 3 row 2 — they're flex-aligned
     by a wrapper-style approach: use justify-self: end and let them
     stack via the row-gap. Actually they need to sit side-by-side
     on the trailing edge, so we use margin-inline-start: auto on
     the expand button + place kebab at the very end. */
  grid-column: 3;
  grid-row: 2;
  align-self: center;
}
.lt-h1.ylc-strip-assigned .ylc-strip-expand-btn {
  /* Assigned strip uses a 4-col grid (col 4 is the chevron-only
     slot). On yardlive, col 3 row 2 holds Start; the chevron sits
     at col 4 next to it. On the dashboard, col 3 row 2 is empty;
     the chevron sits at col 4 anyway, which lands at the strip's
     trailing edge — same visual position as the ongoing strip's
     chevron at col 3 row 2. */
  grid-column: 4;
  grid-row: 2;
  align-self: center;
}
.lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn,
.lt-h1.ylc-strip-assigned .ylc-strip-expand-btn {
  /* Sits at the TRAILING edge — chevron is the natural disclosure
     affordance for an expandable row, so it anchors the row's right
     edge. Kebab sits to its left (see `.ylc-action--more` below). */
  justify-self: end;
  inline-size: 30px;
  block-size: 30px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--bg-raised);
  color: var(--ink-2);
  cursor: pointer;
  transition: transform 180ms ease, background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn:hover,
.lt-h1.ylc-strip-assigned .ylc-strip-expand-btn:hover {
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
  color: var(--brand-700, var(--brand-500));
  border-color: color-mix(in oklab, var(--brand-500) 24%, var(--line));
}
.lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn svg,
.lt-h1.ylc-strip-assigned .ylc-strip-expand-btn svg {
  transition: transform 180ms ease;
}
.lt-h1.ylc-strip-ongoing .ylc-strip-expand-btn.is-on svg,
.lt-h1.ylc-strip-assigned .ylc-strip-expand-btn.is-on svg {
  /* Chevron flips upside-down when expanded — gives a visual cue
     for the toggle state without needing two distinct icons. */
  transform: rotate(180deg);
}
.lt-h1.ylc-strip-ongoing .ylc-action--more {
  /* Pulled ~38px inside the trailing edge so it sits to the LEFT of
     the chevron-expand button. The chevron now anchors the row's
     trailing edge (standard disclosure convention). */
  justify-self: end;
  margin-inline-end: 38px;
  /* Match the expand CTA's radius (both themes). The filled SURFACE is
     light-only (rule below): dark already gives both buttons a matching
     surface via the [data-theme="dark"] group, so overriding it here
     would actually break dark parity. */
  border-radius: var(--r-md);
}
/* Light theme only: give the kebab the expand CTA's filled --bg-raised
   surface (it was transparent, while the adjacent expand chevron was
   filled). Scoped with :not([data-theme="dark"]) so it never overrides
   the dark group's surface, which already matches. */
:root:not([data-theme="dark"]) .lt-h1.ylc-strip-ongoing .ylc-action--more {
  background: var(--bg-raised);
}
/* Accordion content — segment bars span the full width on row 3.
   Only rendered (in JSX) when this strip is the currently-expanded
   one, so the grid implicitly has a third row when needed.
   Border-block-start removed — the segment bars themselves provide
   sufficient visual separation from the stage row above; the divider
   was an extra line that read as visual noise. */
.lt-h1.ylc-strip-ongoing .ylc-strip-segments {
  /* Grid placement only — visual collapsed/expanded styling lives
     in the drawer-slide block below (`.lt-h1.ylc-strip-ongoing
     .ylc-strip-segments` collapsed state + `.is-expanded` open
     state). The padding-block-start and margin-block-start that
     used to live here now animate as part of the drawer slide,
     so they're declared in the collapsed (0) + expanded (6/4)
     states down there. */
  grid-column: 1 / -1;
  grid-row: 3;
}

/* ── Assigned strip variant (list view) ─────────────────────────
   The AssignedCard renders with `lt-h1` chrome when `compact` is
   set, so the visual structure mirrors the ongoing LiveTestStrip:
   same avatar / name / examiner / vehicle chip / status pill /
   attempt / actions rhythm. Overrides below apply only to the
   `.ylc-strip-assigned` modifier so the ongoing strip is untouched. */
.lt-h1.ylc-strip-assigned::before {
  /* Warn-tinted accent stripe (replaces the indigo on ongoing) so
     the leading edge signals "pre-start / waiting" at a glance. */
  background: var(--warn);
}
.lt-h1.ylc-strip-assigned,
[data-theme="dark"] .lt-h1.ylc-strip-assigned {
  /* Standard raised surface — mirrors the card-view AssignedCard
     (`.ylc-assign-card` also uses `var(--bg-raised)`). Both
     selectors carry both `background` and `border-color` because
     the live-test-card.css `[data-theme="dark"] .lt-h1` rule has
     the same (0,2,0) specificity as `.lt-h1.ylc-strip-assigned`
     and loads later — without the boosted variant, the dark-mode
     ongoing-strip surface + border take over and the warn-tinted
     border is lost. */
  background: var(--bg-raised);
  border-color: color-mix(in oklab, var(--warn) 38%, var(--line));
}
.lt-h1.ylc-strip-assigned {
  /* 4-col × 2-row layout:
       row 1: avatar | identity-wrap(1fr)  | Return to waitlist | —
       row 2: veh-chip + Ready ─────────── | Start test         | chevron
     The 4th `auto` column hosts the expand chevron (for the
     maneuver-pills accordion) so it sits AFTER Start without
     colliding with it. On the dashboard (DashAssignedStrip, no
     Return/Start buttons), col 3 is empty and col 4 holds the
     chevron at the strip's trailing edge — same visual outcome.
     Avatar lives only on row 1. */
  grid-template-columns: auto 1fr auto auto;
  grid-template-rows: auto auto;
  /* row-gap: 0 — the 8px gap between row 1 (identity) and row 2
     (row1-info + chevron) is applied via explicit
     `margin-block-start: 8px` on the row-2 items. Same reasoning
     as the ongoing strip — leaving `row-gap: 8px` here adds a
     phantom 8px before the trailing 0-height segments row when
     the accordion is collapsed, AND duplicates the gap before
     the row-2 items (making the assigned strip 8px taller than
     the ongoing strip). See the matching rule in the
     d2-live-grid neighbourhood block above. */
  row-gap: 0;
  /* Clear the 3-row `grid-template-areas` inherited from the
     `@container (max-width: 720px)` rule on `.lt-h1` — without this,
     the named-areas template still declares an empty 3rd row, and
     the row-gap before that phantom row adds ~8px of dead space at
     the strip's bottom. Symmetric padding-block keeps the top and
     bottom gaps identical. */
  grid-template-areas: none;
  padding-block: 10px;
}
.lt-h1.ylc-strip-assigned .lt-h1-avatar {
  grid-column: 1;
  grid-row: 1;
  /* Top-aligned so the avatar sits beside the English name line
     (the first line of the identity wrap) instead of floating to
     the vertical center of the strip. */
  align-self: start;
}
.lt-h1.ylc-strip-assigned .lt-h1-identity-wrap {
  grid-column: 2;
  grid-row: 1;
  /* No `min-inline-size: 0` here — the identity column is the 1fr
     track that should fill the strip width, NOT shrink below
     content. At narrow canvas widths the strip horizontally
     overflows rather than collapsing the name. */
}
.lt-h1.ylc-strip-assigned .ylc-strip-row1-info {
  /* Veh-chip + Ready pair on row 2 LEFT. Spans the full row so it
     doesn't dictate col 3's width (otherwise col 3 would balloon
     to ~205px and squeeze the identity column). justify-self: start
     anchors flush left at the strip's leading edge. */
  grid-column: 1 / -1;
  grid-row: 2;
  display: flex;
  align-items: center;
  gap: 12px;
  min-inline-size: 0;
  justify-self: start;
  align-self: center;
}
.lt-h1.ylc-strip-assigned .ylc-strip-return-btn {
  /* Top-right (col 4 row 1) — the "escape hatch" affordance. The
     strip's grid is now 4 cols (auto 1fr auto auto) since we
     added col 4 for the expand chevron; Return moves to col 4
     too so it stays at the true trailing edge — same visual slot
     it occupied before the chevron was introduced, and analogous
     to the ongoing strip's View-test link position. Return and
     the chevron share col 4 (Return on row 1, chevron on row 2),
     both with justify-self: end so they right-align flush. Same
     32px height floor used across all canvas action buttons. */
  grid-column: 4;
  grid-row: 1;
  justify-self: end;
  align-self: start;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  min-block-size: 32px;
  border: 0;
  background: transparent;
  color: var(--brand-700, var(--brand-500));
  font-size: var(--fsz-label);
  font-weight: 600;
  border-radius: var(--r-sm, 6px);
}
.lt-h1.ylc-strip-assigned .ylc-strip-return-btn:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.lt-h1.ylc-strip-assigned .ylc-strip-start-btn {
  /* Visual styling only — grid placement is owned by the
     `.ylc-strip-actions-trail` wrapper rule below (Start now
     lives inside that flex container, alongside the chevron, so
     it's no longer a direct grid child). The wrapper provides
     both the row-1 → row-2 spacing (margin-block-start) and the
     trailing-edge alignment. */
  justify-self: end;
  align-self: center;
  /* Match the `--cta-h-md` height tier used by the toolbar's
     other actions (kebab, brake, chevron). .btn-primary.btn-sm
     rendered at ~26px otherwise, off-tier from the 32px chevron
     it sits next to in `.ylc-strip-actions-trail`. */
  min-block-size: var(--cta-h-md);
}
/* Trailing action cluster — flex container holding Start + the
   expand chevron on yardlive's compact assigned strip. Spans
   cols 3 → 4 of row 2 so it occupies the full trailing area,
   then flex-aligns its children to the right edge with an 8px
   gap between them. Reproduces the "two adjacent buttons" UX
   without the inter-column gap CSS Grid would otherwise leave
   between separately-placed col-3 and col-4 grid items. */
.lt-h1.ylc-strip-assigned .ylc-strip-actions-trail {
  grid-column: 3 / 5;
  grid-row: 2;
  justify-self: end;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-block-start: 8px;
}
/* Inside the wrapper, individual buttons don't need their own
   margin-block-start (the wrapper provides it) — otherwise the
   margin stacks twice. Buttons keep their existing visual
   styling (border, padding, hover) from elsewhere. */
.lt-h1.ylc-strip-assigned .ylc-strip-actions-trail .ylc-strip-start-btn,
.lt-h1.ylc-strip-assigned .ylc-strip-actions-trail .ylc-strip-expand-btn {
  margin-block-start: 0;
}
/* Ready-to-start pill — warn-tinted, lives as a flex item inside
   .ylc-strip-row1-info, no explicit grid placement. */
.lt-h1.ylc-strip-assigned .ylc-strip-ready {
  background: color-mix(in oklab, var(--warn) 16%, var(--bg-raised));
  color: color-mix(in oklab, var(--warn) 60%, var(--ink));
  border: 1px solid color-mix(in oklab, var(--warn) 30%, var(--line));
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.lt-h1.ylc-strip-assigned .ylc-strip-ready .pill-dot {
  background: var(--warn);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--warn) 25%, transparent);
}

/* ── Assigned-state card ───────────────────────────────────────
   Rendered when a yard-live row has `status: 'assigned'` — student
   has been queued to a vehicle but the test hasn't started yet.
   Visually distinct from the running LiveTestCard (no timer, no
   maneuver segments, no animation) but shares the same header
   chrome and is sized in the same grid cell. The "ready to start"
   warn-tone pill + the Start-test primary CTA combine to signal:
   "this card needs you to take an action." */
/* ── Start-test countdown overlay ────────────────────────────────
   Mounts on top of an AssignedCard during the ~3.6s window between
   the operator clicking "Start test" and the row actually
   transitioning to in_progress. Plays 3 → 2 → 1 → GO (each digit
   ~900ms) so the cause-and-effect of "I just clicked Start on THIS
   card" is unmistakable before the FLIP animation slides the card
   down into the in-progress band. The digit element is keyed by
   the phase so React re-mounts it on each tick — the scale-bounce-
   in animation replays naturally. */
.ylc-countdown-overlay {
  position: absolute;
  /* Skip the AssignedCard's status strip + identity header so the
     student name / avatar / attempt stay visible during the
     countdown. The operator can tell WHICH card they just clicked
     Start on — without this, the 3-2-1-GO floats over a featureless
     blur and the student identity disappears for ~3.6s. The 94px
     covers the warn-tone strip (~36px) plus the head row (~58px). */
  top: 94px;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 6;
  display: grid;
  place-items: center;
  pointer-events: none;
  background: color-mix(in oklab, var(--bg-raised) 80%, transparent);
  backdrop-filter: blur(3px);
  animation: ylc-countdown-overlay-in 220ms ease-out both;
}
.ylc-countdown-digit {
  font-size: 96px;
  line-height: 1;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--brand-500);
  text-shadow: 0 6px 24px color-mix(in oklab, var(--brand-500) 35%, transparent);
  animation: ylc-countdown-digit-in 480ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  user-select: none;
}
.ylc-countdown-overlay.is-go .ylc-countdown-digit {
  /* Final phase — switches tone to success-green and shrinks the text
     because the status word ("Started" / "بدأ") is much wider than a
     single digit and would otherwise overflow narrow cards. */
  color: var(--ok);
  font-size: 48px;
  text-shadow: 0 6px 24px color-mix(in oklab, var(--ok) 40%, transparent);
}
.ylc-assign-card.is-counting-down,
.ylc-strip-assigned.is-counting-down {
  /* Lock interactions while the countdown plays — Return-to-waitlist
     / Start-test buttons can't be clicked again until the row flips
     to in_progress. */
  pointer-events: none;
}
.ylc-strip-assigned.is-counting-down { position: relative; }
/* Dim only the body sections below the head; keep the warn-tone
   "READY TO START" strip + the identity header at full opacity. */
.ylc-assign-card.is-counting-down > :not(.ylc-countdown-overlay):not(.ylc-assign-strip):not(.ylc-assign-head) {
  opacity: 0.35;
  transition: opacity 220ms ease-out;
}
.ylc-strip-assigned.is-counting-down > :not(.ylc-countdown-overlay):not(.lt-h1-avatar):not(.lt-h1-identity-wrap):not(.ylc-strip-view-cta) {
  opacity: 0.35;
  transition: opacity 220ms ease-out;
}
.ylc-strip-assigned .ylc-countdown-overlay {
  /* Full-strip overlay (top: 0) — matches the completion-overlay's
     full-strip treatment on `.ylc-strip-ongoing` above. Previously
     this was 44px (skipping the identity head) but that left the
     countdown digit floating over only the lower half of the row,
     visually inconsistent with the completion flourish on the
     same strip family. Top: 0 + the backdrop-blur on the overlay
     puts a single coherent frosted sheet over the entire strip
     during the countdown — the avatar and identity (kept at full
     opacity by the `.is-counting-down > :not(...)` rule above)
     still read through the blur so the operator can see which
     card is starting. */
  top: 0;
}
/* Strip variant — the assigned strip is much shorter than the
   cards-view card (~80px tall vs ~330px), so shrink the digit
   substantially to fit comfortably inside the row. */
/* Off the type scale on purpose: a countdown display numeral sized for
   prominence, not UI text — same rationale as the LCD timer numerals. */
.ylc-strip-assigned .ylc-countdown-digit {
  font-size: 36px;
}
.ylc-strip-assigned .ylc-countdown-overlay.is-go .ylc-countdown-digit {
  font-size: 24px;
}
@keyframes ylc-countdown-overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes ylc-countdown-digit-in {
  0%   { transform: scale(0.4); opacity: 0; }
  55%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  /* The Start handler short-circuits the countdown entirely under
     reduced-motion, but if a stale animation is mid-flight we still
     freeze its frames so the user isn't bounced. */
  .ylc-countdown-overlay,
  .ylc-countdown-digit { animation: none !important; }
}

.ylc-assign-card {
  background: var(--bg-raised);
  border: 1px solid color-mix(in oklab, var(--warn) 38%, var(--line));
  border-radius: var(--r-md);
  display: flex;
  flex-direction: column;
  position: relative;
  /* Card gap zeroed out — each section now owns its own block
     padding, so the strip, head, meta, langrow, and actions read as
     stacked surfaces with no white slivers between them. Mirrors
     the ongoing LiveTestCard wrapper's no-gap layout. */
  gap: 0;
  box-shadow: var(--sh-xs);
  overflow: hidden;
  /* Container context for the maneuver pills below — at narrow
     card widths (e.g. when the live-canvas column gets cramped),
     5 Arabic pills (العمودي / الجانبي / الموازي / المنحدر / الفرملة)
     would overflow to a second row with generous resting chrome.
     `.ylc-assign-maneuvers` uses fluid `cqi`-based clamp() for
     padding + gap so the chrome scales continuously with the
     card's inline-size — no discrete `@container` step thresholds
     means no visible snap when the viewport scrubs across a
     boundary. An earlier version used discrete `@container` rules
     at 420px / 340px; that produced a visible 1px-viewport flicker
     when a card's computed width crossed the threshold. */
  container-type: inline-size;
  container-name: ylc-assign;
}
/* Full-width status strip across the top of the card — back to
   single-purpose "Ready to start" warn-tone label. The Attempt
   indicator that briefly lived here has moved back into the
   header row (see `.ylc-assign-attempt` below). */
.ylc-assign-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: color-mix(in oklab, var(--warn) 16%, var(--bg-raised));
  color: color-mix(in oklab, var(--warn) 60%, var(--ink));
  font-size: var(--fsz-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-block-end: 1px solid color-mix(in oklab, var(--warn) 30%, var(--line));
}
.ylc-assign-strip-dot {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--warn);
  flex-shrink: 0;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--warn) 25%, transparent);
}
/* Body sections share 16px inline padding so the strip, head,
   facts row, langrow, maneuvers, and actions all align on the
   same vertical gutters as the ongoing LiveTestCard. */
.ylc-assign-card .ylc-assign-facts,
.ylc-assign-card .ylc-assign-langrow,
.ylc-assign-card .ylc-assign-maneuvers,
.ylc-assign-card .ylc-assign-actions {
  padding-inline: 16px;
}
.ylc-assign-card .ylc-assign-actions {
  padding-block-end: 12px;
}
/* Head — cloned from the ongoing LiveTestCard's `.lt-v1-head` so the
   two card states share an identical identity strip: light-grey
   surface, 12×16 padding, 1px bottom rule separating it from the
   body. Same 3-column grid (auto / 1fr / auto) for avatar / name /
   attempt. */
.ylc-assign-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 12px 16px;
  background: var(--bg-raised);
  border-block-end: 1px solid var(--line);
}
.ylc-assign-avatar {
  inline-size: 34px;
  block-size: 34px;
  border-radius: 50%;
  border: 0;
  padding: 0;
  background: var(--bg-sunken);
  cursor: pointer;
  overflow: hidden;
  display: inline-grid;
  place-items: center;
  color: var(--ink-2);
  font-weight: 600;
  font-size: var(--fsz-body);
  /* 1px inset ring like the ongoing card's avatar so the photo edge
     stays defined against the head strip. Sized to match the
     data-table's .density-cozy .student-ava (34px). */
  box-shadow: var(--avatar-ring);
  flex-shrink: 0;
}
.ylc-assign-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
/* Solid brand-500 ring at 2px — matches the test-canvas avatar
   hover (`.tc-identity-avatar:hover`), which is the canonical
   visibility target for "person photo is clickable" across the
   system. Earlier 28%-alpha recipe was too faint against the card
   surface to read as a hover affordance. */
.ylc-assign-avatar:hover { box-shadow: 0 0 0 2px var(--brand-500); }
.ylc-assign-avatar:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.ylc-assign-identity { min-inline-size: 0; }
.ylc-assign-name {
  /* Matches the data-table .student-name .cell-link: 14px / 500,
     line-height 1.2 so the name renders at the same 17px line-box
     as the table cell (was 1.25 → 17.5px → 18px box). */
  font-size: var(--fsz-body);
  font-weight: 500;
  color: var(--ink);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ylc-assign-sub {
  /* Matches the data-table .student-name-sub exactly: caption-tier
     font-size, 400 weight, muted ink, IBM Plex Sans Arabic, and
     line-height 1.3 so the Arabic sub renders at the same 17px
     line-box as the table cell (was browser default normal → ~20px). */
  font-size: var(--fsz-caption);
  font-weight: 400;
  line-height: 1.3;
  color: var(--ink-3);
  font-family: var(--font-ar);
  margin-block-start: 1px;
}
/* The student's sub-name aligns to the LEADING edge of the
   identity column in both directions:
   ──  Page LTR (Arabic sub, `dir="rtl"` on the element) → sub
       reads RTL inside its own box; aligning left makes the
       Arabic word block flush with the Latin name above.
   ──  Page RTL (Latin sub, `dir="ltr"` on the element) → sub
       reads LTR inside its own box; aligning right makes the
       Latin word block flush with the Arabic name above.
   Without these rules the sub drifts to its own intrinsic side
   and reads as a stray label disconnected from the name. */
[dir="ltr"] .ylc-assign-sub { text-align: left; }
[dir="rtl"] .ylc-assign-sub { text-align: right; }
/* Attempt indicator — back in the header's trailing slot, parity
   with the ongoing LiveTestCard's `.lt-v1-head-right`. Compact
   stacked label-over-number; the language toggle moved to its
   own body row (`.ylc-assign-langrow`) so this no longer competes
   for the same column. */
.ylc-assign-attempt {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: end;
  flex-shrink: 0;
  line-height: 1.1;
}
.ylc-assign-attempt-label {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  letter-spacing: 0.02em;
}
.ylc-assign-attempt-num {
  font-size: var(--fsz-h1);
  font-weight: 700;
  color: var(--ink);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* Language toggle now lives on its own body row (see
   `.ylc-assign-langrow`) — full English / العربية labels, no
   abbreviation. The toggle still shares the global `.seg-toggle`
   chrome; this rule just forces a normalised line-height so the
   AR and EN buttons render at identical heights regardless of
   script. */
.ylc-assign-langtoggle {
  flex-shrink: 0;
}
.ylc-assign-langtoggle > .seg-toggle-btn {
  line-height: 1.2;
}
.ylc-assign-status {
  font-size: var(--fsz-caption);
}
/* Combined vehicle + examiner row. Two inline-flex children:
   the vehicle chip (left/leading edge) and the examiner name
   (right/trailing edge with `margin-inline-start: auto`). No
   "Vehicle:" / "Examiner:" labels — the car icon + plate code
   and the user icon + name read on their own at this density,
   and dropping the labels keeps both facts side-by-side without
   forcing the examiner name to ellipsize at narrow widths. */
.ylc-assign-facts {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-block-start: 10px;
  min-inline-size: 0;
}
.ylc-assign-examiner {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-inline-start: auto;
  min-inline-size: 0;
  color: var(--ink);
  font-size: var(--fsz-label);
}
.ylc-assign-examiner svg {
  color: var(--ink-3);
  flex-shrink: 0;
}
/* "Examiner" / "الفاحص" key label — same muted-ink tone as the
   removed `.ylc-assign-meta-key` so the keyed row reads as
   "key: icon name" rather than "icon name floating with no
   context label". */
.ylc-assign-examiner-key {
  color: var(--ink-3);
  flex-shrink: 0;
  letter-spacing: 0.01em;
}
.ylc-assign-examiner-name {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Test-language row. Label on the leading edge ("Test language"
   / "لغة الاختبار"), toggle on the trailing edge. Separator
   border-top creates a soft horizontal rule between the
   facts row above and the language control below, since the
   facts row uses no key/value labels. */
.ylc-assign-langrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-block-start: 8px;
}
.ylc-assign-langrow-key {
  font-size: var(--fsz-label);
  color: var(--ink-3);
  flex-shrink: 0;
  letter-spacing: 0.01em;
}
/* Vehicle chip — local size adjustment over the shared
   `.lt-v1-veh-chip` from live-test-card.css. The ongoing card's
   chip is large + bold + uppercase to dominate the timer block;
   on the smaller assigned card we step the weight + tracking
   down so it sits comfortably as a row value, not as a hero. */
.ylc-assign-veh-chip {
  padding: 3px 8px;
  font-size: var(--fsz-label);
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: var(--r-xs, 6px);
}
/* Maneuvers preview row — pill chips, one per maneuver. Pills
   cluster to the leading edge with a comfortable 8px inter-pill
   gap; trailing slack is left untouched. The earlier
   `space-between` distribution looked good at narrow card widths
   but produced very large empty gaps when the canvas column was
   wide (40-80px gaps between five short pills), which read as
   broken spacing rather than intentional layout. Row gap handles
   vertical breathing when pills wrap. */
.ylc-assign-maneuvers {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Fluid gap — scales with the card's inline-size via cqi
     (container-query units). At card width 480 (cap), col gap
     reaches its 8px ceiling. At card width 316 (floor), col gap
     clamps to 4px. No discrete `@container` steps means no
     visible snap when the viewport scrubs across a threshold
     (previously: 1px viewport delta crossing the 420px container
     threshold snap-flipped pill chrome between two states). */
  gap: clamp(3px, 1.1cqi, 6px) clamp(4px, 1.6cqi, 8px);
  padding-block-start: 8px;
}
/* Live-test page (yardlive) CARD view: the ongoing LiveTestCard grew
   ~a row taller once the per-maneuver timer landed above its segment
   bar. Match the assigned card's height by adding breathing room ABOVE
   and BELOW its maneuver-pill row (the row above is the lang row, the
   row below is the actions row). Scoped to .ylc-grid--cards so the
   dashboard (.d2-live-grid — own spacing) and the strip/list view stay
   untouched. */
.ylc-grid--cards .ylc-assign-card .ylc-assign-maneuvers {
  /* +4px (20→22 / 14→16) so the ready/assign card matches the ongoing
     card, which grew ~4px when the per-maneuver time bumped to 14px. The
     extra height lands as breathing room around the maneuver pills. */
  padding-block-start: 22px;
  padding-block-end: 16px;
}
.ylc-assign-maneuver-pill {
  display: inline-flex;
  align-items: center;
  /* Fluid horizontal padding — clamp between 6px (narrowest cards,
     where 5 Arabic pills only just fit) and 13px (resting on wide
     cards, where there's room to breathe). Same cqi-based scaling
     pattern as `.ylc-assign-maneuvers` gap above; no discrete
     container-query steps, so there's no visible snap at any
     viewport width. Block padding stays a constant 2px — vertical
     compression isn't needed and looks awkward when pills get
     noticeably shorter.

     Slope vs ceiling — the two knobs do different jobs:
       · Slope (2.2cqi) drives padding at MID-width cards (380–
         540px). Above 2.2 the 5-Arabic-pill row starts wrapping;
         this is the empirically-tested safe value.
       · Ceiling (13px) caps padding on wide cards (>591px). This
         is the dial for extra breathing room on roomy cards
         without affecting the wrap-prone mid range. Bumped 8 →
         10 → 12 → 13 as we dialled in the right resting padding.
     */
  padding-block: 2px;
  padding-inline: clamp(6px, 2.2cqi, 13px);
  background: color-mix(in oklab, var(--brand-500) 8%, var(--bg-raised));
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  border-radius: var(--r-pill, 999px);
  /* Aligned with `.lt-v1-seg-label` in live-test-card.css — both
     maneuver labels (pre-start chips + in-flight segment captions)
     now read at the same visual tier (`--fsz-label`: 12px LTR /
     13px RTL). Was hardcoded 11px which made the pre-start pills
     look smaller than the live equivalents. */
  font-size: var(--fsz-label);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  line-height: 1.5;
}
.ylc-assign-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Explicit pinning: Return-to-waitlist anchors to the leading
     edge, Start-test pushes to the trailing edge via auto margin.
     `justify-content: space-between` alone proved unreliable in
     places where the buttons hit min-content width — auto margin
     guarantees the split regardless of text length. */
  padding-block-start: 10px;
}
.ylc-assign-actions .btn-sm { flex: 0 0 auto; }
/* Pin Return to the leading edge: `margin-inline-end: auto` pushes
   Start to the trailing edge; `margin-inline-start: -8px` pulls
   Return's visible content (chevron + label) back into the card's
   16px gutter alignment by compensating the ghost button's 8-9px
   internal inline padding. Net effect: the chevron lines up with
   the maneuver pills above, instead of sitting indented ~25px in
   from the card edge. */
.ylc-assign-return {
  /* L2 text-link CTA — brand text, transparent bg, no border,
     md height floor. Mirrors `.ylc-ongoing-canvas-btn` (cards-view
     "View test") + `.ylt-checkin-btn` + `.lt-h1-cta` so all
     "non-destructive transition" affordances share one visual
     language. See tokens.css `--cta-h-*`. */
  margin-inline-end: auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  min-block-size: var(--cta-h-md);
  border: 0;
  background: transparent;
  color: var(--brand-700, var(--brand-500));
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 600;
  cursor: pointer;
  border-radius: var(--r-sm, 6px);
}
.ylc-assign-return:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.ylc-assign-return:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
.ylc-assign-return svg { flex-shrink: 0; color: currentColor; }
.ylc-assign-start {
  font-weight: 600;
  /* Match the `--cta-h-md` height tier used by the toolbar's
     .ylc-action buttons + .ylc-ongoing-canvas-btn. Without this,
     .btn-primary.btn-sm rendered at ~26px while sibling actions
     sat at 32px — the row read as inconsistent. */
  min-block-size: var(--cta-h-md);
}

/* ── Ongoing-card wrapper ──────────────────────────────────────
   Vertical stack: the production `LiveTestCard` (timer, segments,
   maneuvers) on top, then a small action toolbar below for the
   examiner CTAs (brake / stop / disqualify / open canvas). The
   toolbar is a separate visual block, paired by proximity with
   no gap and a shared background-surface, so the two read as one
   "card + actions" unit without us having to fork the LiveTestCard
   markup. */
/* ── Completion flourish overlay ─────────────────────────────────
   Mounts on top of an OngoingCard / OngoingStripCompact during the
   5000ms window between the Stop / Disqualify / time-exceeded
   action firing and the row leaving the canvas (see
   App.completeAsFail's two-stage logic). Three tonal modes —
   pass / fail / dq — drive the surface tint, ring color, and
   icon color.

   The card behind the overlay dims to ~30% opacity via the
   `.is-completing` modifier so the overlay reads as the primary
   subject without us blanking out the underlying context entirely.

   Two layered animations:
     · `.ylc-completion-ring` — expanding ring pulse, conveys
       "something just happened" energetically (~700ms).
     · `.ylc-completion-icon` — scale-bounce-in (0 → 1.1 → 1.0)
       lands the result symbol crisply at the center.
   Both respect `prefers-reduced-motion`. */
.ylc-completion-overlay {
  position: absolute;
  /* Skip the card's identity header so the student's name, avatar
     and attempt indicator stay visible during the flourish — the
     operator can tell at a glance WHICH card just completed
     (otherwise a full-cover overlay reads as "something just
     happened" but not "to whom"). 62px = approx `.lt-v1-head`
     height (avatar 34px + 12+12 padding + 1px border). The strip
     variant below overrides this with its smaller head height. */
  top: 62px;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  pointer-events: none;
  background: color-mix(in oklab, var(--bg-raised) 88%, transparent);
  backdrop-filter: blur(2px);
  animation: ylc-completion-overlay-in 200ms ease-out both;
}
/* Card-body dim — applies only to the part of the card BELOW the
   head (so the identity stays at full opacity). The LiveTestCard's
   `.lt-v1-body` and `.lt-v1-segments` get dimmed; the head row
   stays crisp so the student / vehicle / attempt are still clearly
   readable. The ongoing-actions toolbar at the bottom also dims so
   the operator doesn't try clicking a brake/kebab during the
   flourish.
   `pointer-events: none` also disables hover/click on the dimmed
   regions — otherwise the maneuver-segment hover tooltips and the
   brake/kebab buttons stay reactive behind the overlay, which is
   confusing (the card looks "frozen" but hover popovers still
   fire). The completion overlay itself stays interaction-free via
   its own `pointer-events: none`. */
.ylc-ongoing-wrap.is-completing .lt-v1-body,
.ylc-ongoing-wrap.is-completing .lt-v1-segments,
.ylc-ongoing-wrap.is-completing .ylc-ongoing-actions {
  opacity: 0.3;
  pointer-events: none;
  transition: opacity 200ms ease-out;
}
/* Hide the "View test" CTA outright during the completion flourish.
   The dimmed (opacity:0.3) action row still rendered the label
   legibly enough to read, which is semantically wrong — the test
   has just completed, "view" no longer applies. Brake stays
   end-anchored via its `margin-inline-start: auto`, so removing
   View test from the flow doesn't shift the other actions. */
.ylc-ongoing-wrap.is-completing .ylc-ongoing-canvas-btn {
  display: none;
}
.ylc-strip-ongoing.is-completing > :not(.ylc-completion-overlay):not(.lt-h1-avatar):not(.lt-h1-identity-wrap):not(.ylc-strip-view-cta) {
  opacity: 0.3;
  pointer-events: none;
  transition: opacity 200ms ease-out;
}
/* Hide the "View test" CTA outright during the completion flourish
   in list view too — matches the cards-view rule above for
   `.ylc-ongoing-canvas-btn`. The test has just completed, "view"
   no longer applies; dimming it to 0.3 still left the label
   legibly readable, which read as semantically wrong. */
.ylc-strip-ongoing.is-completing .ylc-strip-view-cta {
  display: none;
}
/* Ring is now a child of `.ylc-completion-icon`, sized to overlay
   the icon's disc exactly (`inset: 0`) so the two share the same
   center point. The scale animation then expands the ring outward
   from the icon's center — concentric pulse, not the off-center
   drift you get when the ring is an overlay-sibling absolutely
   positioned with default top/left. `infinite` keeps the ring
   pulsing through the full 5s flourish hold. */
.ylc-completion-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid currentColor;
  opacity: 0;
  animation: ylc-completion-ring-pulse 1200ms ease-out infinite;
}
.ylc-completion-icon {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 56px;
  block-size: 56px;
  border-radius: 50%;
  /* Background is set per-tone below (`.is-pass`/`.is-fail`/`.is-dq`)
     so the circle reads green / red against the dimmed card. Was
     `background: currentColor` on the same rule that set
     `color: #fff` — `currentColor` resolved to white in that
     cascade and collapsed the disc into an invisible white-on-white
     icon. Setting the bg explicitly per tone makes the symbol
     visible while keeping the previous layout (small disc + label
     below) intact. */
  animation: ylc-completion-icon-in 360ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  z-index: 1;
}
.ylc-completion-overlay.is-pass .ylc-completion-icon { background: var(--ok); }
.ylc-completion-overlay.is-fail .ylc-completion-icon,
.ylc-completion-overlay.is-dq   .ylc-completion-icon { background: var(--err); }
.ylc-completion-icon svg {
  color: #fff;
  stroke-width: 2.2;
}
.ylc-completion-label {
  position: relative;
  font-size: var(--fsz-body);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  z-index: 1;
  animation: ylc-completion-overlay-in 280ms ease-out 60ms both;
}
.ylc-completion-overlay.is-pass {
  color: var(--ok);
}
.ylc-completion-overlay.is-pass .ylc-completion-label { color: var(--ok); }
.ylc-completion-overlay.is-fail,
.ylc-completion-overlay.is-dq {
  color: var(--err);
}
.ylc-completion-overlay.is-fail .ylc-completion-label,
.ylc-completion-overlay.is-dq .ylc-completion-label { color: var(--err); }
/* Strip variant: the OngoingStripCompact is much shorter than the
   cards-view card, so scale the chrome down so the overlay still
   fits comfortably and doesn't dwarf the strip. */
.ylc-strip-ongoing.is-completing { position: relative; }
.ylc-strip-ongoing .ylc-completion-overlay {
  /* Full-strip overlay (top: 0) with `backdrop-filter: blur` applied
     directly — no head-skip pseudo. The earlier two-element setup
     (transparent overlay + `::before` starting at 44px) created a
     visible seam between the un-blurred head and the blurred body
     beneath, especially in dark mode where the head and overlay
     resolved to different tones.
     One overlay = one coherent frosted surface. The strip's dimmed
     children (opacity 0.3 via the `:not(...)` rule above) read
     through the blur uniformly. The avatar + identity-wrap remain
     full-opacity exceptions, so the operator can still tell which
     student just completed. */
  top: 0;
  flex-direction: row;
  gap: 10px;
  background: transparent;
  backdrop-filter: blur(2px);
}
/* Dark mode: bg-raised mix that worked in light mode (white on
   white) reads as a dark wash on the already-dark strip. Use a
   subtle lift (slate-700 at low opacity) so the overlay reads
   as frosted glass rather than a tinted block. */
[data-theme="dark"] .ylc-completion-overlay {
  background: color-mix(in oklab, #334155 60%, transparent);
}
[data-theme="dark"] .ylc-strip-ongoing .ylc-completion-overlay {
  background: color-mix(in oklab, #334155 40%, transparent);
}
/* Ring inherits its size from the icon (it's now nested inside),
   so no explicit width/height needed here — the strip's smaller
   icon naturally gives a smaller ring. Border width stays at the
   base 2px (was already 2px in the strip override). */
.ylc-strip-ongoing .ylc-completion-icon {
  inline-size: 36px;
  block-size: 36px;
}
.ylc-strip-ongoing .ylc-completion-icon svg {
  inline-size: 18px;
  block-size: 18px;
}
.ylc-strip-ongoing .ylc-completion-label {
  font-size: var(--fsz-label);
}
@keyframes ylc-completion-overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes ylc-completion-icon-in {
  0%   { transform: scale(0.4); opacity: 0; }
  60%  { transform: scale(1.1); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes ylc-completion-ring-pulse {
  /* Ring starts at the icon's exact bounds (scale 1) and expands
     outward to ~2.4x while fading. Both ends at opacity 0 so the
     loop is seamless. Sonar-style pulse emanating from the icon. */
  0%   { transform: scale(1);   opacity: 0; }
  15%  { transform: scale(1.1); opacity: 0.85; }
  100% { transform: scale(2.4); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ylc-completion-overlay,
  .ylc-completion-icon,
  .ylc-completion-ring,
  .ylc-completion-label {
    animation: none !important;
  }
  .ylc-completion-ring { opacity: 0; }
}

.ylc-ongoing-wrap {
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--bg-raised);
  border: 1px solid var(--line-2, oklch(0.86 0.005 250));
  border-radius: var(--r-md);
  box-shadow: var(--sh-xs);
  overflow: hidden;
  /* Position anchor for the leading-accent stripe (::before) and
     the brake-state glow overlay (::after). Unconditional so the
     stripe shows at rest too. */
  position: relative;
}
/* Leading accent stripe — runs the FULL HEIGHT of the wrap
   (header + body + actions row), not just the LiveTestCard
   portion. Indigo at rest; flips to red + pulses when brake is
   engaged (see `.is-brake-active::before` below). The wrap-level
   stripe supersedes the inner `.lt-v1::before` so the stripe no
   longer stops short at the actions row. */
.ylc-ongoing-wrap::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0;
  inline-size: 3px;
  background: var(--brand-600);
  z-index: 1;
  pointer-events: none;
  transition: background 240ms ease;
}
/* LiveTestCard inside the wrap drops its own outer border AND its
   leading stripe — the wrap owns both. Without this, the inner
   card's `::before` would stack over the wrap's stripe for the
   card's height only, creating the very "stops short" appearance
   the user just flagged. */
.ylc-ongoing-wrap > .lt-v1 {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.ylc-ongoing-wrap > .lt-v1::before {
  display: none;
}
/* Timer size override — the default 32px LCD reads as oversized at
   the canvas's two-up density. 24px keeps the ticking MM:SS legible
   while reclaiming card height for the rest of the body. */
.ylc-ongoing-wrap > .lt-v1 .lt-v1-time-num {
  font-size: 24px;
}
/* Brake-engaged breathing glow — when the examiner engages the
   safety brake, the entire wrapper picks up a red inner-border halo
   that gently breathes, modeled on the Claude-Code terminal activity
   indicator. The glow lives on a `::after` overlay (composes on top
   of the wrapper's base chrome without disturbing it). Unified with
   the strip-view brake treatment (see live-test-card.css), so red
   `--err` is the single brake color across both layouts.
   Respects prefers-reduced-motion — steady ring, no pulse. */
.ylc-ongoing-wrap.is-brake-active {
  /* The brake-active stripe + haze draw from --brake-glow — the single dial
     for the brake-engaged tint (red). */
  --brake-glow: var(--err);
  /* Tiny crisp alert ring around the WHOLE card. This wrapper bounds both the
     lt-v1 header/body AND the actions foot, so the ring frames everything (the
     ring on .lt-v1 alone stopped above the foot). The soft breathing haze
     (::after) stays; this is a crisp static edge on top of it. */
  border-color: var(--brake-red);
}
/* Suppress the card's own inner brake border inside the wrapper — the wrapper
   border above is the single ring around the entire card (avoids a double
   ring + a stray red line at the body/foot divider). */
.ylc-ongoing-wrap.is-brake-active .lt-v1.is-brake {
  border-color: transparent;
}
/* Brake-engaged leading stripe — a SOLID accent (no box-shadow bloom).
   It used to pulse its own glow, which made the leading edge glow MORE
   than the other three borders; now the uniform ::after ring is the only
   glow, so all four borders bloom evenly. */
.ylc-ongoing-wrap.is-brake-active::before {
  background: var(--brake-glow);
}
.ylc-ongoing-wrap.is-brake-active::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  /* GLOWING RING — the glow HUGS the --brake-red border (the ring): a hot core
     in the ring's red right at the edge ramps into the lighter --brake-glow
     (--err) emission, fading inward — so the halo reads as the ring glowing,
     not a separate inner haze. base = calm / prefers-reduced-motion steady. */
  box-shadow:
    inset 0 0 6px 0   color-mix(in oklab, var(--brake-red) 48%, transparent),
    inset 0 0 13px 1px color-mix(in oklab, var(--brake-glow) 22%, transparent),
    inset 0 0 28px 3px color-mix(in oklab, var(--brake-glow) 11%, transparent);
  animation: ylc-brake-glow 1.6s ease-in-out infinite;
}
@keyframes ylc-brake-glow {
  /* hot core (--brake-red) hugging the ring → bright --brake-glow emission →
     soft bloom fading inward. Breathes as one. */
  0%, 100% {
    box-shadow:
      inset 0 0 6px 0   color-mix(in oklab, var(--brake-red) 48%, transparent),
      inset 0 0 13px 1px color-mix(in oklab, var(--brake-glow) 22%, transparent),
      inset 0 0 28px 3px color-mix(in oklab, var(--brake-glow) 11%, transparent);
  }
  50% {
    box-shadow:
      inset 0 0 9px 0   color-mix(in oklab, var(--brake-red) 64%, transparent),
      inset 0 0 18px 2px color-mix(in oklab, var(--brake-glow) 32%, transparent),
      inset 0 0 42px 5px color-mix(in oklab, var(--brake-glow) 22%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .ylc-ongoing-wrap.is-brake-active::after { animation: none; }
}
.ylc-ongoing-actions {
  display: flex;
  /* `nowrap` ensures the three actions (View test · Brake · kebab)
     always stay on ONE row, no matter how narrow the card. Was
     `wrap`, which let the trailing kebab drop to a second row at
     certain viewport widths or when a translated label (e.g. AR
     "Release brake") expanded enough to push the trio past the
     card's inner width. With nowrap, the longest action (the
     Brake button with its variable "Engage / Release brake" text)
     truncates via the `min-width: 0` rule below instead of wrapping
     the layout. */
  flex-wrap: nowrap;
  gap: 6px;
  padding: 10px 12px;
  /* White surface — same as the LiveTestCard's body. The card
     already has a grey identity strip up top; a third grey zone at
     the bottom would over-segment the card. A simple 1px line still
     separates the action row from the live-test content above. */
  border-block-start: 1px solid var(--line);
  background: var(--bg-raised);
  align-items: center;
}
.ylc-ongoing-actions .btn-sm { flex: 0 0 auto; }
/* Brake button is the only action that owns a translatable label
   wider than the card might allow at extreme narrow widths. Letting
   it shrink (min-width: 0 + truncation) keeps the toolbar on one
   row when nowrap meets a too-narrow card; View test (text-link)
   and the kebab stay at their natural sizes. */
.ylc-ongoing-actions .brake-btn--card {
  flex: 0 1 auto;
  min-inline-size: 0;
  overflow: hidden;
}
.ylc-ongoing-actions .brake-btn--card .brake-glyphs > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Footer layout: navigation leads, live actions trail.
   "View test" (the text-link) sits on the LEADING edge; the brake
   control + kebab (⋯) form a cluster on the TRAILING edge — the card's
   "action zone" where the eye finishes in LTR. The brake leads the
   cluster (it is the consequential control); the kebab trails at the
   very edge (overflow-menu convention), which also keeps it clear of
   the brake's resting position.

   Implementation: DOM order is already View test → brake → kebab, all
   at the default `order: 0`. `margin-inline-end: auto` on View test
   opens the gap that pushes the brake + kebab cluster to the trailing
   edge.

   Visual result, LTR:  [View test] _________________ [brake] [⋯]
   Visual result, RTL:  [⋯] [brake] _________________ [View test]
   (logical-property margin + flex flip automatically under RTL).

   ── Rollback path ────────────────────────────────────────────────
   Restore "brake-kebab leading / View test trailing" by setting the
   rule below back to `order: 1; margin-inline-start: auto;` and giving
   `.ylc-ongoing-actions .ylc-action--more` `order: -1`. */
.ylc-ongoing-actions .ylc-ongoing-canvas-btn {
  margin-inline-end: auto;
}
/* Kebab (⋯) trails at the toolbar's edge — default order:0, last in
   DOM order, so no override needed. Overflow-menu convention, and it
   keeps the kebab clear of the brake's resting position. */
/* "View live test" — text-only brand link sitting on the leading
   edge of the toolbar. No fill, no border, so the AssignedCard's
   primary "Start test" stays the only solid-brand button in the
   canvas; the two card states then read with the right hierarchy.
   The external-link icon trailing the label signals "opens in a
   different surface" (the live-test detail view). */
.ylc-ongoing-canvas-btn {
  /* L2 text-link CTA — see `.ylc-assign-return` for the family.
     Padding stays tight at 4×6; --cta-h-md does the height lifting. */
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  min-block-size: var(--cta-h-md);
  border: 0;
  background: transparent;
  color: var(--brand-700, var(--brand-500));
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 600;
  cursor: pointer;
  border-radius: var(--r-sm, 6px);
  flex: 0 0 auto;
}
.ylc-ongoing-canvas-btn:hover {
  color: var(--brand-800, var(--brand-700));
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
}
.ylc-ongoing-canvas-btn:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
.ylc-ongoing-canvas-btn svg {
  flex-shrink: 0;
  /* Subtle horizontal nudge on hover to reinforce the "go" affordance */
  transition: transform 160ms ease;
}
/* External-link icon doesn't get a directional hover nudge — the
   icon already conveys "opens elsewhere" without motion. */

/* ── Serious-action button variants (.ylc-action) ──────────────
   Used on the ongoing-card toolbar for the brake / stop /
   disqualify trio. These actions have consequences (test cancel,
   student outcome), so they get an explicit colored treatment
   instead of the neutral ghost. The .btn defaults are overridden
   per variant. */
.ylc-action {
  border-width: 1px;
  border-style: solid;
  background: transparent;
  font-weight: 600;
  /* Toolbar height parity — every `.ylc-action` (brake, kebab,
     strip-expand chevron) aligns to the `--cta-h-md` floor so the
     row reads as a single tier. Without this floor, .btn-sm
     padding produced ~26-30px buttons inconsistent with the
     icon-only kebab (which explicitly sets block-size). The text
     buttons still have their own padding-inline; min-block-size
     just sets the height baseline. Matches `.ylc-ongoing-canvas-btn`
     and `.ylc-assign-start` / `.ylc-strip-start-btn` (each set
     independently below). */
  min-block-size: var(--cta-h-md);
}
/* Stop + Disqualify — destructive outline. Red text + red border
   over a transparent base reads as "this is dangerous" at rest
   without dominating the toolbar. Solid danger fill on hover when
   the user actually moves toward the action. Currently used inside
   the kebab popover; preserved for any inline destructive button
   we might add later. */
.ylc-action--danger {
  color: var(--err);
  border-color: color-mix(in oklab, var(--err) 38%, var(--line));
}
.ylc-action--danger:hover {
  background: var(--err);
  border-color: var(--err);
  color: var(--brand-ink);
}
.ylc-action--danger:focus-visible {
  outline: 2px solid var(--err);
  outline-offset: 2px;
}

/* ── Kebab "more actions" button ──────────────────────────────
   Neutral icon-only button that sits in the toolbar where Stop +
   Disqualify used to live. Square-ish footprint, no label text.
   Opens a portaled popover with the destructive items. */
.ylc-action--more {
  /* md icon-only tier — matches sibling toolbar action buttons
     (Engage brake / Start test / View test) so the icon-only
     kebab doesn't read as a stepped-down option in the row.
     See tokens.css `--cta-h-*`. */
  inline-size: var(--cta-h-md);
  block-size: var(--cta-h-md);
  padding-inline: 0;
  padding-block: 0;
  display: inline-grid;
  place-items: center;
  color: var(--ink-2);
  border-color: var(--line);
}
/* Brand-tinted hover — matches the standard hover treatment used by
   the View-live-test text CTA and ghost buttons elsewhere in the
   project. Background is brand-500 mixed at 8% into transparent,
   text tints to brand-700, border picks up a brand-tinted hairline. */
.ylc-action--more:hover {
  background: color-mix(in oklab, var(--brand-500) 8%, transparent);
  color: var(--brand-700, var(--brand-500));
  border-color: color-mix(in oklab, var(--brand-500) 24%, var(--line));
}
.ylc-action--more.is-open {
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  color: var(--brand-700, var(--brand-500));
  border-color: color-mix(in oklab, var(--brand-500) 32%, var(--line));
}

/* ── More-actions popover ─────────────────────────────────────
   Portaled to document.body so it escapes `.ylc-ongoing-wrap`'s
   overflow: hidden (per the iVE floating-tooltips rule). Anchored
   by inline `position: fixed` + top/left from the button rect.
   Each item carries its own destructive treatment — red text + red
   icon at rest, red fill on hover so the consequence reads clearly
   the moment the user moves toward it. */
.ylc-more-menu {
  z-index: var(--z-popover, 1200);
  min-inline-size: 168px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-md, 0 8px 24px rgba(15, 23, 42, 0.12));
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ylc-more-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border: 0;
  background: transparent;
  border-radius: calc(var(--r-md) - 4px);
  color: var(--ink);
  font: inherit;
  font-size: var(--fsz-label);
  font-weight: 500;
  cursor: pointer;
  text-align: start;
  inline-size: 100%;
}
.ylc-more-item svg { flex-shrink: 0; }
.ylc-more-item:hover { background: var(--bg-sunken); }
.ylc-more-item:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: -2px;
}
.ylc-more-item--danger {
  color: var(--err);
}
.ylc-more-item--danger:hover {
  background: color-mix(in oklab, var(--err) 12%, var(--bg-raised));
  color: var(--err);
}
.ylc-more-item--danger:focus-visible {
  outline-color: var(--err);
}

/* `.main` is a flex column with `gap: var(--s-3)` (12px). We want
   a smaller breathing gap between adjacent sections — pull the
   second section up so the effective gap reads as ~s-2 (8px). */
.ylt-section + .ylt-section { margin-block-start: calc(var(--s-2) - var(--s-3)); }
/* Yardlive section heads match the system-wide table frame standard
   (1.5px, `--tbl-frame-border` token color). Border-block-end stays 0
   when expanded (the head visually merges into the table card below)
   and flips to a full bottom border when collapsed so the head reads
   as a standalone rounded strip. Standardized with the rest of the
   table system (see `.table-card` + token comment) — no longer
   scoped via `.ylt-split-tables`. */
.ylt-section .ltp-section-head {
  border: 1.5px solid var(--tbl-frame-border);
  border-block-end: 0;
  border-start-start-radius: var(--r-lg);
  border-start-end-radius: var(--r-lg);
  background: var(--bg-sunken);
}
.ylt-section .ltp-section-head.is-collapsed {
  border-end-start-radius: var(--r-lg);
  border-end-end-radius: var(--r-lg);
  border-block-end: 1.5px solid var(--tbl-frame-border);
}
/* Very faded seam between the (expanded) section head and the table's
   column-header row below it. The head + table read as one framed
   surface, but a whisper-thin divider keeps the section title visually
   distinct from the column headers — especially while the head is
   stuck and rows scroll beneath. Only when expanded (collapsed heads
   already carry a full bottom border above). */
.ylt-section .ltp-section-head:not(.is-collapsed) {
  border-block-end: 1px solid color-mix(in oklab, var(--line) 70%, transparent);
}
/* Table card directly inside an .ylt-section omits its top border
   so the head and the card read as one continuous framed surface.
   Width / color come from the base `.table-card` rule. */
.ylt-section .table-card {
  border-block-start-width: 0;
}
/* Expanded table sits flush with the section head above —
   flat top corners on both .table-card AND the inner thead's
   first/last th (otherwise thead's rounded corners would carve
   into the head→table seam and expose page bg). */
.ylt-section .table-card {
  border-start-start-radius: 0;
  border-start-end-radius: 0;
}
.ylt-section .data-table thead th:first-child,
.ylt-section .data-table thead th:last-child {
  border-start-start-radius: 0;
  border-start-end-radius: 0;
}
/* Strict column widths via `table-layout: fixed` — the YLT tables
   need predictable column widths so the surplus space (from a
   shrunken floor + a wider 2fr:3fr left-col share, or from the user
   hiding toggleable columns) routes to the Student column rather
   than being smeared across every column equally. In `auto` layout,
   max-inline-size on td/th is ignored when the table has more room
   than the sum of its content; `fixed` honors the col widths
   strictly, and any unallocated space falls to the single auto col
   (Student). */
.ylt-section .data-table {
  table-layout: fixed;
  /* Tables fill their container via the inherited `.data-table { width:
     100% }`. Both Pending and Completed have IDENTICAL declared column
     widths (set in pendingOverrides + completedOverrides in
     yardlive.html), so table-layout: fixed distributes any container
     surplus the same way across both — rendered widths drift a few
     px from declared values (e.g. 116 → 119) but the two tables stay
     column-aligned with each other.
     Tried explicit width:602px to lock declared widths exactly — that
     fixed fragments but left a massive empty gap on the right when the
     container was wider than 602px. Filling the container is more
     important than pixel-exact declared widths. */
}
/* The Type column's colored dot (vehicle-tag::before) is hidden in
   the YLT tables — with the Status pill carrying its own dot in
   the column right next to Type, two adjacent dots competed for
   visual attention. Other pages (Saved Tests, Reports) keep the
   dot since they don't have the Status pill alongside. */
.ylt-section .vehicle-tag::before {
  display: none;
}
/* Per-section stripe color override — Pending defaults to brand,
   Completed (or anything tagged data-stripe="neutral") gets a
   muted ink stripe so the historical surface visually steps back. */
.ylt-section[data-stripe="neutral"] .ltp-section-head::before {
  background: var(--ink-3);
}

/* Status pill (Pending section's Status column). Three values:
     pending      — student hasn't arrived (muted)
     checked_in   — arrived, awaiting assignment (brand — action moment)
     assigned     — vehicle assigned, walking to it (ok — in motion) */
/* `.ylt-status-pill` styling MIGRATED to `.pill` + tone variants
   (is-neutral / is-info / is-ok). Class names kept as addressing hooks
   on the markup but carry no styling here — the `.pill` primitive
   owns all visual rules. */

/* Assign-vehicle CTA in the actions column (Pending rows where
   status === 'checked_in'). Reuses .btn .btn-primary chrome but
   constrains size + alignment to fit the actions cell. */
.ylt-assign-btn {
  inline-size: auto;
  white-space: nowrap;
  /* Tinted CTA — overrides `.btn-primary`'s solid brand-600
     background with a soft brand-tint pill so the row-level
     action reads as clearly interactive without competing with
     the table content. Matches the design system's "active /
     selected" vocabulary (brand-500 @ 12% bg + brand-tint @ 22%
     border + brand-700 text). Hover lifts one tier. */
  background: color-mix(in oklab, var(--brand-500) 12%, transparent);
  color: var(--brand-700);
  border: 1px solid color-mix(in oklab, var(--brand-500) 22%, var(--line));
  box-shadow: none;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.ylt-assign-btn:hover {
  background: color-mix(in oklab, var(--brand-500) 18%, transparent);
  border-color: color-mix(in oklab, var(--brand-500) 32%, var(--line));
  color: var(--brand-800);
}
.ylt-assign-btn:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
}

/* Queue table — tightens the global .data-table for this page */
.data-table.ltp-queue-table { table-layout: auto; }
.data-table.ltp-queue-table th,
.data-table.ltp-queue-table td { padding: var(--s-2) var(--s-3); }
.data-table.ltp-queue-table .th-id      { inline-size: 110px; }
.data-table.ltp-queue-table .th-time    { inline-size: 80px;  }
.data-table.ltp-queue-table .th-status  { inline-size: 110px; }
.data-table.ltp-queue-table .th-type    { inline-size: 80px;  }
.data-table.ltp-queue-table .th-action  { inline-size: 110px; text-align: end; }
.data-table.ltp-queue-table .ltp-action-cell { text-align: end; }
.data-table.ltp-queue-table .student-name {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-inline-size: 0;
}
.data-table.ltp-queue-table .row-photo {
  inline-size: 28px;
  block-size: 28px;
  border-radius: var(--r-pill);
  overflow: hidden;
  display: inline-grid;
  place-items: center;
  background: var(--brand-tint-2);
  color: var(--brand-700);
  font: var(--fs-caption);
  flex: 0 0 auto;
  box-shadow: var(--avatar-ring);
}
.data-table.ltp-queue-table .row-photo img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}
.data-table.ltp-queue-table .row-photo-fallback {
  display: inline-grid;
  place-items: center;
  inline-size: 100%;
  block-size: 100%;
}
.ltp-empty-row {
  padding: var(--s-6) !important;
  text-align: center;
  color: var(--ink-3);
  font: var(--fs-body-sm);
}

/* Inline status pill (queue-row state) */
.ltp-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  padding: 3px 8px;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  white-space: nowrap;
}
.ltp-status-dot {
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}
.ltp-status-pill.is-ok {
  background: var(--ok-tint);
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 22%, var(--line));
}
.ltp-status-pill.is-warn {
  background: var(--warn-tint);
  color: var(--warn);
  border-color: color-mix(in oklab, var(--warn) 26%, var(--line));
}
.ltp-status-pill.is-planned {
  background: var(--bg-sunken);
  color: var(--ink-3);
  border-color: var(--line);
}

/* Assign panel — expanded row beneath the pending student */
.ltp-assign-row > td {
  padding: 0 !important;
  background: color-mix(in oklab, var(--brand-500) 3%, var(--bg-raised));
}
.ltp-assign-panel {
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.ltp-assign-label {
  font: var(--fs-body-sm);
  color: var(--ink-2);
  line-height: 1.5;
}
.ltp-assign-label strong { color: var(--ink); font-weight: 600; }
.ltp-assign-empty {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  padding: var(--s-3);
  background: var(--warn-tint);
  border: 1px solid color-mix(in oklab, var(--warn) 26%, var(--line));
  border-radius: var(--r-sm);
  color: var(--ink);
}
.ltp-assign-empty svg { color: var(--warn); flex-shrink: 0; margin-block-start: 1px; }
.ltp-assign-empty-sub {
  font: var(--fs-body-sm);
  color: var(--ink-3);
  margin-block-start: 2px;
}
.ltp-assign-actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}
.ltp-assign-lang-wrap {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-inline-end: auto;
}
.ltp-assign-lang-lbl {
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Vehicle picker grid + tiles */
.ltp-veh-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--s-2);
}
.ltp-veh-tile {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
  text-align: start;
  transition: border-color var(--t-fast), background var(--t-fast),
              box-shadow var(--t-fast);
}
.ltp-veh-tile:hover:not(:disabled) {
  border-color: var(--brand-500);
  background: var(--brand-tint);
}
.ltp-veh-tile.is-selected {
  border-color: var(--brand-600);
  background: var(--brand-tint);
  box-shadow: 0 0 0 1px var(--brand-600);
}
.ltp-veh-tile.is-disabled,
.ltp-veh-tile:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  background: var(--bg-sunken);
}
.ltp-veh-tile-icon {
  position: relative;
  flex: 0 0 auto;
  inline-size: 40px;
  block-size: 40px;
  display: grid;
  place-items: center;
  background: var(--bg-sunken);
  border-radius: var(--r-sm);
  color: var(--ink-2);
}
.ltp-veh-tile-slot {
  position: absolute;
  inset-block-start: -4px;
  inset-inline-end: -4px;
  inline-size: 16px;
  block-size: 16px;
  display: grid;
  place-items: center;
  background: var(--brand-600);
  color: var(--brand-ink);
  font: var(--fs-caption);
  font-weight: 700;
  border-radius: var(--r-pill);
}
.ltp-veh-tile-body { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.ltp-veh-tile-id {
  font: var(--fs-body-strong);
  letter-spacing: var(--ls-body-strong);
  color: var(--ink);
  font-family: var(--font-mono);
}
.ltp-veh-tile-state {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
}
.ltp-veh-tile-state.is-ok    { color: var(--ok); }
.ltp-veh-tile-state.is-warn  { color: var(--warn); }
.ltp-veh-tile-state.is-err   { color: var(--err); }
.ltp-veh-tile-dot { inline-size: 6px; block-size: 6px; border-radius: 50%; background: currentColor; }
.ltp-veh-tile-fuel {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font: var(--fs-caption);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* Language toggle (used in AssignPanel + PreStartCard — extends .btn) */
.ltp-lang-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.ltp-flag {
  display: inline-flex;
  inline-size: 16px;
  block-size: 11px;
  border-radius: 2px;
  overflow: hidden;
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--ink) 10%, transparent);
}

/* ── Live zone (right column when split) ─────────────────────── */
.ltp-live-zone {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  min-inline-size: 0;
}
.ltp-live-section {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.ltp-live-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-block-end: 1px solid var(--line);
}
.ltp-live-section-title {
  margin: 0;
  font: var(--fs-h3);
  letter-spacing: var(--ls-h3);
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}
.ltp-live-section-count {
  font: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--ink-3);
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
}
.ltp-live-section-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
}
.ltp-live-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-6) var(--s-4);
  text-align: center;
  color: var(--ink-3);
  font: var(--fs-body-sm);
}
.ltp-live-empty svg { color: var(--ink-4); }

/* ── Avatar (used in PreStartCard) ───────────────────────────── */
.ltp-avatar {
  display: inline-grid;
  place-items: center;
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--brand-tint-2);
  color: var(--brand-700);
  font: var(--fs-caption);
  text-transform: uppercase;
  box-shadow: var(--avatar-ring);
  flex: 0 0 auto;
}
.ltp-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.ltp-avatar-22 { inline-size: 22px; block-size: 22px; font-size: var(--fsz-chart-label); }
.ltp-avatar-40 { inline-size: 40px; block-size: 40px; font: var(--fs-label); }
.ltp-avatar.is-examiner { background: var(--brand-tint-2); color: var(--brand-700); }

/* ── Pre-start card (Not Started state) ─────────────────────── */
.ltp-prestart {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--bg-raised);
  box-shadow: var(--sh-xs);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: box-shadow var(--t-fast);
}
.ltp-prestart:hover { box-shadow: var(--sh-sm); }
.ltp-prestart-head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  flex-wrap: wrap;
}
.ltp-prestart-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; flex: 1 1 200px; }
.ltp-prestart-name {
  font: var(--fs-body-strong);
  letter-spacing: var(--ls-body-strong);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ltp-prestart-meta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: var(--fs-caption);
  color: var(--ink-3);
}
.ltp-prestart-meta .cell-id.mono {
  font: var(--fs-caption);
  letter-spacing: 0;
  font-family: var(--font-mono);
  color: var(--ink-3);
  font-weight: 500;
}
.ltp-prestart-dot { color: var(--ink-4); }
.ltp-prestart-status {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  flex-wrap: wrap;
}
.ltp-prestart-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0 var(--s-4) var(--s-3);
  flex-wrap: wrap;
}
.ltp-prestart-actions .btn-primary {
  flex: 1 1 160px;
  justify-content: center;
}
.ltp-prestart-foot {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border-block-start: 1px dashed var(--line);
  background: color-mix(in oklab, var(--ink) 2%, var(--bg-raised));
  flex-wrap: wrap;
}
.ltp-prestart-examiner {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: var(--fs-body-sm);
  color: var(--ink-2);
  margin-inline-end: auto;
}

/* ── Ongoing tile (LiveTestCard + examiner controls) ────────── */
.ltp-ongoing-tile {
  display: flex;
  flex-direction: column;
  border-radius: var(--r-md);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--bg-raised);
  box-shadow: var(--sh-xs);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.ltp-ongoing-tile:hover { box-shadow: var(--sh-sm); }
/* Strip the LiveTestCard's own border/shadow when nested inside an
   ongoing tile — the outer tile already provides the chrome. */
.ltp-ongoing-tile > .lt-v1 {
  border: 0;
  box-shadow: none;
  border-radius: 0;
}
.ltp-ongoing-controls {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border-block-start: 1px dashed var(--line);
  background: color-mix(in oklab, var(--ink) 2%, var(--bg-raised));
  flex-wrap: wrap;
}
.btn.ltp-action-danger { color: var(--err); }
.btn.ltp-action-danger:hover {
  background: var(--err-tint);
  color: var(--err);
}

/* ── Toast ───────────────────────────────────────────────────── */
.ltp-toast-wrap {
  position: fixed;
  inset-block-end: var(--s-6);
  inset-inline-end: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  z-index: var(--z-toast);
  pointer-events: none;
}
.ltp-toast {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  background: var(--ink);
  color: var(--bg-raised);
  border-radius: var(--r-sm);
  box-shadow: var(--sh-md);
  font: var(--fs-body-sm);
  pointer-events: auto;
  animation: ltp-toast-in 200ms cubic-bezier(.4,0,.2,1);
}
@keyframes ltp-toast-in {
  from { transform: translateY(8px); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ltp-toast { animation: none; }
}

/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 720px) {
  .data-table.ltp-queue-table .th-time,
  .data-table.ltp-queue-table .th-status,
  .data-table.ltp-queue-table .th-type,
  .ltp-queue-row td:nth-child(2),
  .ltp-queue-row td:nth-child(4),
  .ltp-queue-row td:nth-child(5) { display: none; }
  .ltp-queue-search.filter-search { flex: 1 1 100%; max-inline-size: none; }
}

