/* ══════════════════════════════════════════
   Manuall Design System
   ══════════════════════════════════════════ */

/* ══════════════════════════════════════════
   Cascade Layers - vendor CSS isolation
   ──────────────────────────────────────────
   Pre-register the "vendor" layer so third-party CSS (Leaflet, Event
   Calendar, future Pell/Frappe CSS) loaded via loadLib() lands in this
   layer via `@import url(...) layer(vendor)`.

   How this kills "specificity tied + vendor loads later" bugs forever:
   per CSS spec, UNLAYERED rules win over ANY layered rule regardless
   of specificity or source order. app.css is unlayered → app rules
   automatically beat every vendor rule with zero ceremony. No more
   `body .leaflet-container ...` specificity crutches.

   The order below puts `reset` BELOW `vendor` so vendor's specific
   selectors (Leaflet's `.leaflet-popup-content { margin: 13px 24px }`,
   Event Calendar's spacing, etc.) win over our `*` box-model reset.
   Without this, the universal reset (0,0,0) would beat every vendor
   rule because unlayered rules > any layer - which it shouldn't, since
   the reset's job is "default for our own elements", not "kill vendor
   defaults". App component overrides stay unlayered above both. */
@layer reset, vendor;

/* ══════════════════════════════════════════
   Fluid sizing vs. media queries - when to use what
   ──────────────────────────────────────────
   clamp(min, preferred, max)   Smooth, continuous scale of a value with
                                viewport (font-size, padding, sidebar width).
                                Always anchor the preferred term in rem -
                                clamp(1rem, 0.9rem + 0.4vw, 1.25rem) - never
                                pure vw, which breaks browser zoom.
   min() / max()                "Cap at whichever is smaller". Modal width:
                                max-width: min(760px, 92vw); width: 100%;
   minmax(min(100%, N), 1fr)    Grid tracks that never overflow when the
                                container itself is narrower than N. Use on
                                every repeat(auto-fit/auto-fill, ...) - zero
                                visual cost, eliminates nested-layout overflow.
   @container                   Component styles itself based on its OWN
                                width, not the viewport. Reach for this when
                                the same component renders in multiple
                                contexts of different widths (KPI card in a
                                6-col dashboard row vs. a 3-col report grid
                                vs. a narrow detail panel). Units: cqw / cqi
                                = 1% of container inline size. Notes:
                                  (1) container-type: inline-size creates a
                                      new containing block - fine for us
                                      because popovers / dialogs use the
                                      browser top layer and are immune.
                                  (2) Do NOT use cqi for sizing properties
                                      (padding, width) on auto-sized
                                      containers - it's a circular dep and
                                      resolves to the clamp max. cqi is
                                      only safe for properties that don't
                                      feed back into layout (e.g. font-size
                                      on a child of the container, not
                                      padding on the container itself).
   @media                       KEEP for genuine mode switches (sidebar
                                off-canvas, row↔column flip, show↔hide) and
                                accessibility constraints (44px touch targets).
                                Do NOT use for smooth, continuous scaling.
                                Use RANGE syntax - @media (width <= 768px) -
                                not legacy min-width/max-width. Ranges compose
                                naturally: @media (640px < width <= 1024px).
   dvh / svh / lvh              Dynamic viewport height units. Always prefer
                                dvh over vh for full-height layouts on mobile
                                - vh counts the area under the browser chrome
                                (URL bar, bottom toolbar) as viewport, so
                                "100vh" gets cut off when chrome is visible.
                                dvh resizes smoothly as chrome shows/hides;
                                svh = smallest (chrome visible); lvh = largest
                                (chrome hidden). Use dvh for heights, svh if
                                you need a guaranteed "never-gets-cut" floor,
                                lvh rarely. Works in Chrome 108+, Safari 15.4+,
                                Firefox 101+.
   ══════════════════════════════════════════ */

/* Alpine.js x-cloak: hide elements before Alpine initializes */
[x-cloak] { display: none !important; }

/* ── Hidden Scrollbars (global) ──
   Scroll functionality preserved; visual scrollbar hidden on all elements. */
* {
    scrollbar-width: none;           /* Firefox */
    -ms-overflow-style: none;        /* IE/Edge legacy */
}
*::-webkit-scrollbar {
    display: none;                   /* Chrome, Safari, Opera */
}

/* Eliminate white flash between page navigations - must match --bg */
html {
    background: var(--bg);
}

/* View Transitions API - fast crossfade on content swap */
#content-area { view-transition-name: content-area; }
::view-transition-old(content-area) { animation: 80ms ease-out both vtFadeOut; }
::view-transition-new(content-area) { animation: 80ms ease-in both vtFadeIn; }
@keyframes vtFadeOut { to { opacity: 0; } }
@keyframes vtFadeIn { from { opacity: 0; } }
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
.sidebar, .top-bar, .app-footer, dialog, dialog::backdrop, .sidebar-overlay,
.toast-container, [popover], #image-preview-dialog { view-transition-name: none; }
/* During request: pointer-events disabled to prevent double-clicks, no visual dim */
.htmx-request #content-area { pointer-events: none; }
/* Mitigates double-click on a slow form posting twice and creating
   two rows. While HTMX has the request in flight, dim and disable the
   submit button so a second click is a no-op. Doesn't help against
   scripted concurrency (Promise.all of two POSTs); that needs server-
   side idempotency keys, tracked separately. */
form.htmx-request button[type="submit"],
form.htmx-request input[type="submit"] {
    opacity: 0.6;
    pointer-events: none;
}
/* Animated progress bar at top of viewport during any HTMX request */
.sidebar-nav.htmx-request::before,
.top-bar.htmx-request::before {
    content: '';
    position: fixed;
    top: 0; left: 0; right: 0;
    height: 3px;
    background: linear-gradient(90deg, transparent, var(--primary), transparent);
    background-size: 200% 100%;
    animation: progressSlide 0.8s ease-in-out infinite;
    z-index: var(--z-toast);
}
@keyframes progressSlide {
    0% { background-position: 100% 0; }
    100% { background-position: -100% 0; }
}
/* Loading indicator.
   `.loading-spinner` defines `display: inline-block`,
   which by source-order cascade was beating the `.htmx-indicator
   { display: none }` rule (same single-class specificity, later
   wins). The result: every spinner was permanently visible regardless
   of the htmx-request state. Combined `.htmx-indicator.loading-spinner`
   selectors below bump specificity (0,2,0) to win unambiguously. */
.htmx-indicator { display: none; }
.htmx-indicator.loading-spinner { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator,
.htmx-request .htmx-indicator.loading-spinner,
.htmx-request.htmx-indicator.loading-spinner { display: inline-block; }
.loading-spinner {
    width: 16px; height: 16px;
    border: 2px solid var(--border);
    border-top-color: var(--primary);
    border-radius: var(--radius-full);
    animation: spin 0.6s linear infinite;
    display: inline-block;
    vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Working scrim ──────────────────────────────────────────────
   Full-viewport blocking loader for a slow, NAVIGATING POST (e.g. building
   an evaluation tenant, which runs the full demo seed). The form submit shows
   this so the user sees activity instead of a frozen page while the request is
   in flight; the PRG redirect that lands replaces the document and clears it.
   Fixed + high z so it sits over page content; reuses .loading-spinner. */
.working-scrim {
    position: fixed;
    inset: 0;
    z-index: var(--z-tooltip);
    display: grid;
    place-items: center;
    padding: 1.5rem;
    background: color-mix(in srgb, var(--bg) 78%, transparent);
    backdrop-filter: blur(2px);
}
.working-scrim[hidden] { display: none; }
.working-scrim-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 40px rgb(0 0 0 / 0.18);
    padding: 2rem 2.25rem;
    max-width: min(420px, 92vw);
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.85rem;
}
.working-scrim .loading-spinner { width: 42px; height: 42px; border-width: 3px; }
.working-scrim-title { margin: 0; font-family: var(--font-display); color: var(--heading); font-size: var(--step-2); }
.working-scrim-status { margin: 0; color: var(--text); min-height: 1.4em; }
.working-scrim-hint { margin: 0; color: var(--text-muted); font-size: var(--text-sm); }

/* Hide unprocessed Lucide <i> tags - prevents flash before icon renders */
i[data-lucide] { display: inline-block; width: 1em; height: 1em; }

/* ── Branded Loading Indicator ── */
.brand-loader {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    padding: 2.5rem 1rem;
    color: var(--text-muted);
    font-size: var(--text-sm);
}
.brand-loader-icon {
    width: 36px;
    height: 36px;
    background: var(--primary);
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    animation: brandPulse 1.2s ease-in-out infinite;
}
.brand-loader-icon img {
    width: 22px;
    height: auto;
    filter: brightness(0) invert(1);
}
@keyframes brandPulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.5; transform: scale(0.92); }
}
/* Compact variant for inline/search contexts */
.brand-loader-sm {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--text-muted);
    font-size: var(--text-sm);
}
.brand-loader-sm img {
    width: 18px;
    height: auto;
    color: var(--primary);
    animation: brandPulse 1.2s ease-in-out infinite;
}

:root {
    --primary: #696CFF;
    --primary-hover: #5A5CE6;
    --primary-light: rgba(105, 108, 255, 0.12);
    --primary-rgb: 105, 108, 255;
    --success: #2E7D32;
    --success-light: rgba(46, 125, 50, 0.10);
    --warning: #FFAB00;
    --warning-light: rgba(255, 171, 0, 0.12);
    --danger: #EF4444;
    --danger-light: rgba(239, 68, 68, 0.12);
    /* Tonal action variants - bolder than the brand --danger / --success /
       --warning so 1px borders and small icons stay legible against the card
       background. The full set ({hue}-strong, -hover, -tint) lets every
       semantic button class follow the same shape: tinted at rest, filled
       on hover. Used by .btn-delete / .btn-icon.danger (existing) and the
       new .btn-success / .btn-warning siblings. Picking the right tone is
       how a button signals what it DOES - primary blue is for the page's
       single prominent action; constructive (approve, mark paid) gets
       success green; cautionary (suspend, void) gets warning amber;
       destructive (delete) gets danger red. */
    --danger-strong: #DC2626;
    --danger-strong-hover: #B91C1C;
    --danger-strong-tint: rgba(220, 38, 38, 0.08);
    --danger-strong-tint-stronger: rgba(220, 38, 38, 0.12);
    --success-strong: #16A34A;
    --success-strong-hover: #15803D;
    --success-strong-tint: rgba(22, 163, 74, 0.08);
    --warning-strong: #D97706;
    --warning-strong-hover: #B45309;
    --warning-strong-tint: rgba(217, 119, 6, 0.08);
    /* Subtle hover tints for actions-panel items. Same hue as --danger /
       brand success; opacity tuned so the hover is visible without dominating. */
    --danger-subtle-hover: rgba(239, 68, 68, 0.08);
    --danger-border-subtle: rgba(239, 68, 68, 0.22);
    --success-subtle-hover: rgba(113, 221, 55, 0.08);
    --success-border-subtle: rgba(113, 221, 55, 0.22);

    /* ── Z-index scale ──
       Semantic layers ordered low → high. Use these instead of raw numbers so
       it's obvious what a rule is stacking against. Each layer has headroom
       (50-100 gap) for future additions without shifting the whole scale. */
    --z-sticky:   20;    /* in-content sticky bars (bulk-action-bar) */
    --z-topbar:   50;    /* sticky page topbar */
    --z-sidebar: 100;    /* fixed sidebar (desktop) */
    --z-overlay: 150;    /* mobile sidebar backdrop */
    --z-tooltip: 200;    /* tooltips + mobile sidebar panel (sits above its overlay) */
    --z-dropdown: 300;   /* header dropdowns (sidebar hover tooltip) */
    --z-popover: 500;    /* floating panels (actions-panel) */
    --z-toast:  9999;    /* toasts, progress bar, notifications */
    --z-skip:  10000;    /* a11y skip-link - always on top */

    /* Transition-duration scale. Use semantic names so motion feels coherent
       across the app and we can adjust globally if needed. */
    --transition-fast: 100ms;   /* micro-interactions (hover tints, icon pops) */
    --transition-base: 150ms;   /* default - most hovers and small swaps */
    --transition-slow: 250ms;   /* larger swaps (page view-transitions, card slides) */

    --info: #03C3EC;
    --info-light: rgba(3, 195, 236, 0.12);
    /* Extended semantic colors used by calendar / status legends / charts.
       Light + dark themes both use these same hex values (intentional -
       they're status semantics, not theme-surface colors). */
    --purple: #8B5CF6;
    --cyan: #06B6D4;
    --orange: #F97316;
    --bg: #F5F5F9;
    --card: #FFFFFF;
    --card-hover: #F9FAFC;
    --sidebar: #2B2C40;
    --sidebar-hover: #32334A;
    --sidebar-text: #A3A4CC;
    --sidebar-active-bg: rgba(105, 108, 255, 0.16);
    --sidebar-brand-text: #CFD0E7;
    --sidebar-section: #5D5E82;
    --text: #697A8D;
    --heading: #566A7F;
    --border: #E7E7E8;
    --text-muted: #8592A3;
    --border-light: #f4f4f5;
    --input-bg: var(--card);
    --input-border: var(--border);
    --shadow: 0 2px 6px rgba(67, 89, 113, 0.12);
    --shadow-hover: 0 6px 18px rgba(67, 89, 113, 0.16);
    --shadow-sm: 0 1px 3px rgba(67, 89, 113, 0.08);
    --shadow-card: 0 2px 8px rgba(67, 89, 113, 0.08);
    --shadow-card-hover: 0 4px 16px rgba(67, 89, 113, 0.12);
    --shadow-focus-ring: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
    /* Sharp/hybrid look: rectangular surfaces are square (0); the two accent
       shapes (capsule pills, full circles) stay curved on purpose so they read
       as deliberate against the sharp grid. Bump these off 0 to soften again. */
    --radius-xs: 0;
    --radius-sm: 0;
    --radius: 0;
    --radius-card: 0;
    --radius-lg: 0;
    --radius-pill: 2rem;  /* kept curved: capsule accent - chat inputs, .btn-pill */
    --radius-full: 50%;   /* kept curved: avatars, status dots, icon circles */

    /* Card padding scale - single source of truth so card variants share a
       coherent spacing rhythm. Mobile overrides drop everything by one step. */
    /* Fluid card padding - scales with viewport (responsive-first; rem-anchored
       per the fluid rule so zoom still works). Kept ordered default>dense>tight
       at every width so the three tiers never cross. */
    --pad-card:       clamp(1.125rem, 1rem + 0.5vw, 1.5rem);   /* default 18->24px */
    --pad-card-dense: clamp(1rem, 0.9rem + 0.4vw, 1.25rem);    /* dense   16->20px */
    --pad-card-tight: clamp(0.875rem, 0.8rem + 0.3vw, 1rem);   /* tight   14->16px */

    /* ── Fluid type scale (viewport-linear interpolation, 320→1440 px) ──
       Anchors: min=320 px / max=1440 px / cap=2560 px.
       Formula: preferred = intercept_rem + slope_vw, where
         slope_vw = (Δpx / 1120) × 100  and
         intercept_rem = (min_px − slope_px × 320) / 16
       Both anchors verified exact; clamp cap locks size above 1440 px. */
    --step-5:  clamp(1.75rem,  1.607rem + 0.714vw, 2.75rem);   /* h1: 28→36 px, cap 44 */
    --step-4:  clamp(1.375rem, 1.268rem + 0.536vw, 2.125rem);  /* h2: 22→28 px, cap 34 */
    --step-3:  clamp(1.125rem, 1.054rem + 0.357vw, 1.625rem);  /* h3: 18→22 px, cap 26 */
    --step-2:  clamp(1rem,     0.946rem + 0.268vw, 1.375rem);  /* h4: 16→19 px, cap 22 */
    --step-1:  clamp(0.875rem, 0.839rem + 0.179vw, 1.125rem);  /* h5: 14→16 px, cap 18 */
    --step-0:  clamp(0.875rem, 0.839rem + 0.179vw, 1.0625rem); /* body: 14→16 px, cap 17 */
    --step--1: clamp(0.75rem,  0.732rem + 0.089vw, 0.875rem);  /* small: 12→13 px, cap 14 */
    --step--2: clamp(0.625rem, 0.607rem + 0.089vw, 0.75rem);   /* micro: 10→11 px, cap 12 */

    /* Letter-spacing (em - scales proportionally with fluid font-size).
       Negative tightens large display type; positive opens small captions. */
    --ls-display:  -0.025em;
    --ls-heading:  -0.015em;
    --ls-subhead:  -0.01em;
    --ls-body:      0em;
    --ls-small:     0.01em;
    --ls-caps:      0.08em;

    /* Line-height (unitless - scales with font-size at every step). */
    --lh-display:  1.15;
    --lh-heading:  1.25;
    --lh-subhead:  1.35;
    --lh-body:     1.6;
    --lh-ui:       1.4;
    --lh-tight:    1.2;

    /* Font-weight map - both fonts are now variable (full wght axis).
       Intermediate values (e.g. 620, 450) are valid: the variable font renders
       them precisely, unlike static fonts which snap to the nearest instance.
       Note: font-weight: clamp() is invalid CSS (vw is a length, weight is a
       number - incompatible types). Viewport-responsive weight uses breakpoints
       below instead of a continuous clamp. */
    /* Variable-font axis stops, calibrated for Public Sans (variable wght
       100-900). The "named" weights are tuned slightly below the canonical
       CSS round numbers so each step reads as a gentle increment from the
       --fw-body 380 baseline rather than a jarring jump. The hierarchy is
       preserved (light < normal < medium < semibold < bold) but compressed
       so emphasis feels purposeful, not shouty.

       Reference: at 380 body, +80 axis points is a clearly-distinct
       weight change; +120 starts to read as "different paragraph". The
       previous 500/600/700 stops produced +120/+220/+320 jumps from
       body - too aggressive. The new 460/540/620 stops produce
       +80/+160/+240 - proportional but lighter overall. */
    --fw-normal:   400;
    --fw-medium:   460;   /* was 500 - .fw-500 detail-page values */
    --fw-semibold: 540;   /* was 600 - .fw-600 bold totals / identity */
    --fw-bold:     620;   /* was 700 - .fw-700 strongest emphasis */
    /* Body weight - set ONCE on <body> and inherited everywhere. 380
       sits between light (300) and normal (400) on Public Sans's
       variable wght axis: polished, legible, "normal-thin". Inner
       surfaces (tables, forms, quick-view, KB) do NOT redeclare
       font-weight - they inherit this. Headings opt up explicitly
       via --fw-heading; buttons/badges/.fw-500-700 utilities carry
       emphasis where genuinely needed. Single source of truth. */
    --fw-body:     380;
    /* Calibrated heading weights: slightly below named stops so Poppins-trained
       eyes read them as "about semibold / about bold" at the sizes they appear. */
    --fw-display:  700;   /* h1: full bold */

    /* UI font-size scale - fully fluid. Compact tier (--text-sm/base/md)
       clamps from a small floor on phone to a comfortably-larger ceiling on
       4K displays. Tables, badges, btn-sm, chat sidebar, pipeline cards
       all consume --text-base and scale together. The two micro tokens
       (--text-xxs, --text-xs) stay fixed because they pin badge/timestamp
       padding where pixel stability matters more than viewport scaling.
       Font-WEIGHT is independent - --fw-body 380 thinness applies regardless
       of size, preserving the polished neat look at every breakpoint. */
    --text-xs:   0.7rem;     /* caption-small - fixed */
    --text-sm:   clamp(0.75rem, 0.722rem + 0.089vw, 0.8125rem);  /* caption / secondary: 12→13 px */
    --text-body: clamp(0.875rem, 0.839rem + 0.125vw, 1rem);      /* main-body / form text: 14→16 px */
    /* Form input - same curve as --text-base by design (compact controls
       match list density). Kept as a separate alias so future input-only
       tweaks don't ripple through tables. */
    /* --transition kept as a shorthand alias for the common "base duration + ease"
       pattern used in hover colour/background transitions. New code should prefer
       the duration tokens (--transition-fast/base/slow) defined below. */
    --transition: var(--transition-base) ease;
    --transition-btn: background-color var(--transition-base) ease, color var(--transition-base) ease, border-color var(--transition-base) ease;
    /* Sidebar width scales with viewport - narrower on small laptops, wider on
       4K displays. Floor 240 keeps labels + icons readable; ceiling 300 caps so
       the sidebar never dominates the content area on ultrawide. */
    --sidebar-width: clamp(240px, 18vw, 300px);
    --topbar-height: 62px;
    --font-body: 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-display: 'Plus Jakarta Sans', 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

    --white: #fff;

    /* Elevation */
    --shadow-md: 0 4px 12px rgba(0,0,0,0.15);
    --shadow-lg: 0 8px 24px rgba(0,0,0,0.18);
    --shadow-xl: 0 8px 30px rgba(0,0,0,0.2), 0 2px 8px rgba(0,0,0,0.1);

    /* Badge color tokens (light mode) - overridden in [data-theme="dark"] */
    --badge-primary-bg: rgba(105, 108, 255, 0.12);
    --badge-primary-fg: #696CFF;
    --badge-success-bg: rgba(46, 125, 50, 0.10);
    --badge-success-fg: #2E7D32;
    --badge-danger-bg: rgba(255, 62, 29, 0.12);
    --badge-danger-fg: #FF3E1D;
    --badge-warning-bg: rgba(255, 171, 0, 0.12);
    --badge-warning-fg: #FFAB00;
    --badge-info-bg: rgba(3, 195, 236, 0.12);
    --badge-info-fg: #03C3EC;
    --badge-purple-bg: rgba(139, 92, 246, 0.12);
    --badge-purple-fg: #8B5CF6;
    --badge-muted-bg: rgba(133, 146, 163, 0.15);
    --badge-muted-fg: #8592A3;
    --badge-orange-bg: rgba(249, 115, 22, 0.12);
    --badge-orange-fg: #F97316;

    /* Touch target (WCAG 2.1) */
    --touch-min: 44px;
}

/* ── Reset & Base ──
   Wrapped in @layer reset so vendor CSS (Leaflet's .leaflet-popup-content
   margin, Event Calendar's day-cell padding, etc.) wins over the universal
   margin/padding reset. Without the layer, unlayered `*` (specificity 0,0,0)
   would beat every vendor rule by virtue of being unlayered. */
@layer reset {
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
}

html {
    /* Smooth scroll for in-page anchors and any element.scrollIntoView()
       call (chat, global-search navigation, tab panels). No-op for users
       with prefers-reduced-motion per spec. */
    scroll-behavior: smooth;
    /* Theme-aware scrollbars + thin width. In dark mode the browser paints
       them dark; in light mode, light. scrollbar-color is widely supported
       (Baseline 2024); the track uses --bg so it disappears into the page. */
    scrollbar-color: var(--border) transparent;
    scrollbar-width: thin;
    /* Tells the UA to render built-in UI (form controls, scrollbars,
       context menus, date pickers) using the current theme's light/dark
       scheme. Without this, a dark-mode page still shows a *light* date
       picker popup. Declared on :root so [data-theme="dark"] can override. */
    color-scheme: light;
}
html[data-theme="dark"] { color-scheme: dark; }

body {
    font-family: var(--font-body);
    /* Fluid body size - was fixed --text-md (15px) which felt static on
       wide monitors and crowded on phones. --text-body clamps from 14px
       on phone up to 17px on large displays, keeping line-length and
       reading rhythm comfortable at any viewport. Carries the spirit of
       fluid typography app-wide; surfaces that need fixed compact sizes
       (lists, tables, forms) override explicitly via --text-base /
       --text-input so they stay scannable. */
    font-size: var(--text-body);
    color: var(--text);
    background: var(--bg);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    /* Tokenized via --fw-body (380) - interpolated between light (300)
       and normal (400). Lightens the overall app feel without dipping
       into "hard to read" territory. Headings, buttons, badges, and
       any element with an explicit font-weight rule are unaffected. */
    font-weight: var(--fw-body);
    font-variation-settings: 'wght' var(--fw-body);
    transition: font-variation-settings 180ms ease;
    /* Native form controls (checkbox, radio, progress, range) pick up the
       brand purple instead of the OS default blue. One line, global win. */
    accent-color: var(--primary);
}

/* Branded text selection - replaces the OS default blue highlight. */
::selection { background: var(--primary-light); color: var(--heading); }

/* HTMX swaps can change content ABOVE the current scroll position (e.g.
   a list refresh after a delete), which normally causes the browser to
   auto-scroll to keep the visible content stable - but that "jumps" the
   viewport from the user's POV. Disabling overflow-anchor on the swap
   target preserves the user's scroll position instead. */
#page-content, #content-area { overflow-anchor: none; }

h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-display);
    color: var(--heading);
    text-wrap: balance;
}
/* Body copy + descriptions get the lighter "pretty" algorithm: same
   orphan prevention, but optimized for longer runs of text. */
p, .meta-sub, .text-muted, .empty-desc { text-wrap: pretty; }
h1, h2, h3, h4, h5, h6 { transition: font-variation-settings 180ms ease; }
h1 {
    font-size: var(--step-5);
    line-height: var(--lh-display);
    letter-spacing: var(--ls-display);
    font-weight: var(--fw-display);
    font-variation-settings: 'wght' var(--fw-display);
}
h2 {
    font-size: var(--step-4);
    line-height: var(--lh-heading);
    letter-spacing: var(--ls-heading);
    font-weight: var(--fw-bold);
    font-variation-settings: 'wght' var(--fw-bold);
}
h3 {
    font-size: var(--step-3);
    line-height: var(--lh-heading);
    letter-spacing: var(--ls-heading);
    font-weight: var(--fw-bold);
    font-variation-settings: 'wght' var(--fw-bold);
}
h4 {
    font-size: var(--step-2);
    line-height: var(--lh-subhead);
    letter-spacing: var(--ls-subhead);
    font-weight: var(--fw-semibold);
    font-variation-settings: 'wght' 580;
}
h5 {
    font-size: var(--step-1);
    line-height: var(--lh-subhead);
    letter-spacing: var(--ls-subhead);
    font-weight: var(--fw-medium);
    font-variation-settings: 'wght' 520;
}
h6 {
    font-size: var(--step--1);
    line-height: var(--lh-ui);
    letter-spacing: var(--ls-small);
    font-weight: var(--fw-medium);
    font-variation-settings: 'wght' 500;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Prevent SVG icons inside buttons/links from stealing clicks */
button svg, a svg, [role="button"] svg,
button i, a i, [role="button"] i { pointer-events: none; }

a { color: var(--primary); text-decoration: none; transition: color var(--transition); }
a:hover { color: var(--primary-hover); }
a:not([role="button"]):not(.ghost):hover { text-decoration: underline; }

p { margin-bottom: 1rem; }
small { font-size: var(--text-sm); }
strong { font-weight: 600; }
hr { border: none; border-top: 1px solid var(--border); margin: 1rem 0; }

/* ── Cards ── */
.card {
    background: var(--card);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: var(--pad-card);
    margin-bottom: 1rem;
}
/* Long-form reading surface (help/docs): the .card primitive + a roomier,
   bordered, more-rounded treatment. Padding scales further than a data card
   and is rem-anchored (the old .help-article used pure 2.6vw, which broke zoom). */
.card--doc {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: clamp(1.25rem, 1rem + 1.25vw, 2.5rem);
    margin-bottom: 0;
}
.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--border);
}
.card-header h3, .card-header h4 { margin: 0; }

article {
    background: var(--card);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: var(--pad-card-dense);
    margin-bottom: 1rem;
}
article header {
    padding: 0;
    margin-bottom: 0.5rem;
}

/* ── Buttons ── */
/* Exclude Event Calendar (.ec-), Pell (.pell-) toolbar, and Leaflet
   (.leaflet-) map controls. Leaflet's zoom anchors + custom map markers
   carry role="button" but expect their own minimal styling (30x30 white
   control, anchored map-pin markers). Without the opt-out the global
   [role="button"] rules below paint them solid --primary purple pills,
   which produced a "stickers stuck to the map" visual. */
/* The :not() chain stays semantically required (it keeps third-party
   widgets, btn-pill, the markdown editor toolbar, etc. out of the
   solid-pill button theme), but it's wrapped in :where() so the chain
   contributes ZERO specificity. Pre-fix the long :not chain pushed the
   selector to specificity 0,6,0 - which silently outranked .btn-sm /
   .btn-xs / page-header overrides. .btn-sm wanted 0.25rem 0.6rem
   padding; the base rule's 0.5rem 1.25rem won; every "small" button
   in the app actually rendered at full size. Now the selector is just
   0,1,0 and tighter variants override cleanly. */
:is(button, [role="button"], .btn, .outline, .btn-sm):where(:not(:is([class*="ec-"], .ec-button, .btn-pill, .chat-back, .label-help, .picker-v2-row, .form-sections-dot, .form-sections-arrow, .star-btn, .emoji-btn, .rec-btn, .eff-btn, .slot-row, [class*="leaflet-"], .md-editor-toolbar *))) {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    /* Compact-by-default per design preference. Mobile touch-target rule
       still enforces min-height: 40px so finger targets stay accessible. */
    padding: 0.35rem 0.8rem;
    font-size: var(--text-sm);
    font-weight: 500;
    font-family: inherit;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    cursor: pointer;
    transition: var(--transition-btn);
    text-decoration: none;
    line-height: 1.3;
    white-space: nowrap;
    vertical-align: middle;
}

[role="button"]:where(:not(:is(.secondary, .outline, .ghost, .btn-icon, .btn-pill, .chat-back, .label-help, .picker-v2-row, [class*="ec-"], [class*="leaflet-"]))),
button:where(:not(:is(.secondary, .outline, .ghost, .btn-icon, .btn-pill, .tab-btn, .chat-back, .label-help, .picker-v2-row, [class*="ec-"], .md-editor-toolbar *, .form-sections-dot, .form-sections-arrow, .star-btn, .emoji-btn, .rec-btn, .eff-btn, .slot-row))) {
    background: var(--primary);
    color: var(--white);
    border-color: var(--primary);
}
[role="button"]:where(:not(:is(.secondary, .outline, .ghost, .btn-icon, .btn-pill, .chat-back, .label-help, [class*="ec-"], [class*="leaflet-"]))):hover,
button:where(:not(:is(.secondary, .outline, .ghost, .btn-icon, .btn-pill, .tab-btn, .chat-back, .label-help, [class*="ec-"], .md-editor-toolbar *, .form-sections-dot, .form-sections-arrow, .star-btn, .emoji-btn, .rec-btn, .eff-btn, .slot-row))):hover {
    background: var(--primary-hover);
    border-color: var(--primary-hover);
    text-decoration: none;
    /* Subtler lift than the previous 0 2px 8px rgba(--primary-rgb, 0.4)
       glow. The dramatic blue halo on every hover was a big contributor to
       blue-color fatigue across screens with many primary buttons. The
       soft neutral shadow says "raised" without staining the surrounding
       pixels with brand color. */
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

/* Button interaction states */
button:active:not(:disabled),
[role="button"]:active:not(:disabled):not([class*="leaflet-"]) {
    transform: scale(0.97);
}
/* Keyboard-only focus ring for buttons/links lives in the consolidated
   "Focus & Accessibility Enhancements" block (one source of truth for the
   whole interactive set: a, button, input, select, [role=button], etc.). */
button[aria-busy="true"] {
    position: relative;
    color: transparent !important;
    pointer-events: none;
}
button[aria-busy="true"]::after {
    content: '';
    position: absolute;
    width: 16px; height: 16px;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    border: 2px solid rgba(255,255,255,0.3);
    border-top-color: var(--white);
    border-radius: var(--radius-full);
    animation: btn-spin 0.6s linear infinite;
}
@keyframes btn-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }

.secondary, button.secondary {
    background: var(--text-muted);
    color: var(--white);
    border-color: var(--text-muted);
}
.secondary:hover, button.secondary:hover {
    background: var(--heading);
    border-color: var(--heading);
    text-decoration: none;
}

.outline, button.outline, [role="button"].outline {
    background: transparent;
    color: var(--primary);
    border-color: var(--primary);
}
.outline:hover, button.outline:hover, [role="button"].outline:hover {
    background: var(--primary);
    color: var(--white);
    text-decoration: none;
}

.secondary.outline, button.secondary.outline {
    color: var(--text-muted);
    border-color: var(--text-muted);
    background: transparent;
}
.secondary.outline:hover, button.secondary.outline:hover {
    background: var(--text-muted);
    color: var(--white);
}

.ghost { background: transparent; border: none; color: var(--text); padding: 0.35rem 0.5rem; cursor: pointer; }
.ghost:hover { background: var(--primary-light); color: var(--primary); }

/* Uniform topbar icon buttons - standalone class, no .ghost dependency */
.topbar-icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; padding: 0; border-radius: var(--radius-sm);
    cursor: pointer; transition: background var(--transition), color var(--transition), border-color var(--transition);
    color: var(--heading);
    background: var(--border-light);
    border: 1px solid var(--border);
    text-decoration: none;
}
/* Compact topbar items (weather, offline-status) that show text labels */
.topbar-icon-btn.topbar-compact {
    width: auto; min-width: auto;
    padding: 0.35rem 0.5rem; gap: 0.25rem;
    font-size: var(--text-sm);
    font-weight: var(--fw-medium);
    font-variation-settings: 'wght' var(--fw-medium);
}
/* Topbar unread-badge override - smaller than sidebar variant.
   Positioning lives here too so the markup doesn't need an inline
   style="position:absolute;top:-2px;right:-2px;" - keeps the open/close
   toggle purely class-based (hidden ↔ visible) without competing
   inline-style state. */
.topbar-unread.unread-badge { min-width: 16px; height: 16px; font-size: var(--text-xs); padding: 0 4px; position: absolute; top: -2px; right: -2px; }
/* :hover shared with .btn-icon via :is() in the Buttons section */
.topbar-icon-btn svg { width: 18px; height: 18px; }

/* auth-required modifier for the offline-status badge.
   When the queue carries rows that failed with 401/403 (session
   expired while offline), the topbar promotes the badge to a warning
   color so the tech sees their work didn't disappear and knows to
   sign back in. Uses the existing --warning token; no hardcoded
   hex (per design-system principle). */
.topbar-icon-btn.offline-status-btn--auth-required,
.topbar-icon-btn.offline-status-btn--auth-required:hover {
    background: var(--warning-light);
    color: var(--warning);
    border-color: var(--warning);
}

/* ─────────────────────────────────────────────────────────────────
   Topbar dropdown menus.

   Mirrors the .actions-menu pattern used on every list/detail page:
   `position: relative` wrapper holds trigger + panel; the panel is
   `position: absolute; right: 0; top: calc(100% + 4px)` so it
   anchors to the wrapper's bottom-right corner. Show/hide is driven
   by Alpine x-show + @click.outside - same plumbing as .actions-menu.
   No native Popover API, no anchor-positioning machinery, no JS
   spatial math.
   ───────────────────────────────────────────────────────────────── */
.topbar-menu {
    position: relative;
    display: inline-flex;
}
.topbar-popover {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    z-index: var(--z-popover);
    border: 1px solid var(--border);
    background: var(--card);
    border-radius: var(--radius);
    box-shadow: var(--shadow-hover, 0 4px 16px rgba(0, 0, 0, 0.12));
    padding: 0.5rem 0;
    /* Width sized so the labels fit without horizontal scroll. Two
       widths: standard items (220px), tenant switcher rows (260px). */
    min-width: 220px;
    color: var(--text);
}
.topbar-popover.topbar-popover--wide { min-width: 260px; }

/* Phase Bug-6 - at the narrow phone tier (<=480px) the topbar
   was packing 8-9 items and clipping the avatar off the right
   edge. Drop weather (cosmetic) and chat (lower-traffic than
   notifications); keeps logo, search, +, theme, notifications,
   tenant-pill, avatar visible. Both stay reachable: weather is
   informational only, chat is in the sidebar nav. */
@media (width <= 480px) {
    #topbar-weather,
    #topbar-chat { display: none; }
}

.topbar-popover-heading {
    padding: 0.4rem 1rem 0.5rem;
    font-size: var(--text-xs, 0.7rem);
    font-weight: 600;
    text-transform: uppercase;
    color: var(--text-muted);
    letter-spacing: 0.05em;
}

.topbar-popover-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    color: var(--text);
    text-decoration: none;
    font-size: var(--text-sm);
    background: transparent;
    border: none;
    width: 100%;
    text-align: left;
    cursor: pointer;
    transition: background var(--transition);
}
/* Real :hover/:focus-visible - replaces the prior
   onmouseover="this.style.background=..." inline mutation that broke
   keyboard navigation. */
.topbar-popover-item:hover,
.topbar-popover-item:focus,
.topbar-popover-item:focus-visible {
    background: var(--primary-light);
    color: var(--text);
    text-decoration: none;
    outline: none;
}
.topbar-popover-item.is-current {
    font-weight: 600;
    background: var(--primary-light);
}
.topbar-popover-item--danger {
    color: var(--danger);
}
.topbar-popover-item--danger:hover,
.topbar-popover-item--danger:focus,
.topbar-popover-item--danger:focus-visible {
    background: color-mix(in srgb, var(--danger) 8%, transparent);
    color: var(--danger);
}
.topbar-popover-item--muted {
    color: var(--text-muted);
    font-size: var(--text-xs, 0.8rem);
}
.topbar-popover-divider {
    height: 1px;
    background: var(--border);
    margin: 0.4rem 0;
}
.topbar-popover-tenant-meta {
    padding: 0.5rem 1rem 0.6rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 0.25rem;
}
.topbar-popover-tenant-name {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--heading);
    line-height: 1.2;
}
.topbar-popover-tenant-role {
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-top: 0.15rem;
}

/* Small button variant.
   :not(.btn-pill) brings specificity to 0,2,0 to match the base
   button rule at line 585 (which includes `.btn-sm:not(.btn-pill)`
   in its big selector). Without this match-up, the base rule's
   0.5rem 1.25rem padding overrode .btn-sm's intended 0.25rem 0.6rem
   - so every "small" button in the app was actually rendering at
   full button size. Now that .btn-sm is defined later AND at equal
   specificity, the cascade lets .btn-sm win. */
.btn-sm:not(.btn-pill) { padding: 0.25rem 0.6rem; font-size: var(--text-sm); }
/* Extra-small button for dense action rows (trash restore/delete, tax-rate
   inline edit, availability approve/deny, etc.). Smaller font + tighter
   padding than .btn-sm; used where a full .btn-sm is too heavy visually. */
.btn-xs { padding: 0.2rem 0.5rem; font-size: var(--text-sm); }

/* Icon-only action button for tables */
/* Small icon buttons (edit, download, etc.) - height matches regular buttons */
.btn-icon {
    width: 32px;
    height: 32px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    cursor: pointer;
    transition: var(--transition-btn);
    text-decoration: none;
    position: relative;
}
button.btn-icon {
    background: none;
    border: 1px solid var(--border);
    font: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}
/* Shared icon-control hover (btn-icon + topbar-icon-btn): primary tint */
:is(.btn-icon, .topbar-icon-btn):hover {
    border-color: var(--primary);
    color: var(--primary);
    background: var(--primary-light);
    text-decoration: none;
}
.btn-icon svg, .btn-icon i[data-lucide] { width: 15px; height: 15px; }
/* Text-content icon buttons (e.g. × character) need legible font sizing */
.btn-icon.danger:not(:has(svg)):not(:has(i)) { font-size: 16px; font-weight: 700; line-height: 1; }

/* Tone buttons - delete / success / warning share ONE shape; the modifier
   class only sets which --*-strong token drives it, so a dev picks the verb
   (delete vs complete vs pause), not a different visual pattern. Tinted bg +
   colored 1px stroke at rest, solid fill + white glyph on hover. --btn-tone
   inherits to the child svg/i, so the stroke + glyph rules resolve per
   button. Uses --danger/success/warning-strong (bolder than the base hue) so
   the 1px stroke reads clearly on a white card; dark-mode keeps the hue at
   higher luminance via the [data-theme="dark"] token redefinitions. */
.btn-delete,  .btn-icon.danger  { --btn-tone: var(--danger-strong);  --btn-tone-tint: var(--danger-strong-tint); }
.btn-success, .btn-icon.success { --btn-tone: var(--success-strong); --btn-tone-tint: var(--success-strong-tint); }
.btn-warning, .btn-icon.warning { --btn-tone: var(--warning-strong); --btn-tone-tint: var(--warning-strong-tint); }

.btn-delete, .btn-icon.danger,
.btn-success, .btn-icon.success,
.btn-warning, .btn-icon.warning {
    border-color: var(--btn-tone);
    color: var(--btn-tone);
    background: var(--btn-tone-tint);
}
.btn-delete:hover, .btn-icon.danger:hover,
.btn-success:hover, .btn-icon.success:hover,
.btn-warning:hover, .btn-icon.warning:hover {
    background: var(--btn-tone);
    color: var(--white);
    border-color: var(--btn-tone);
}
/* White glyph on hover (icon+text and icon-only variants) */
.btn-delete:hover :is(svg, i), .btn-icon.danger:hover :is(svg, i),
.btn-success:hover :is(svg, i), .btn-icon.success:hover :is(svg, i),
.btn-warning:hover :is(svg, i), .btn-icon.warning:hover :is(svg, i) { color: var(--white); }
/* Icon-only variant: svg stroke follows the tone at rest, white on hover */
.btn-icon.danger svg, .btn-icon.success svg, .btn-icon.warning svg { stroke: var(--btn-tone); }
.btn-icon.danger:hover svg, .btn-icon.success:hover svg, .btn-icon.warning:hover svg { stroke: var(--white); }

/* Table actions cell */
.table-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    align-items: center;
}
/* When buttons are grouped inside .table-actions, normalize their height,
   padding, and icon size so mixing variants (btn-sm, btn-icon, btn-delete,
   ghost, outline, plain danger) doesn't produce a ragged row. This is the
   single source of truth for inline row-action sizing; using !important here
   is intentional - a grouped row must look like one control strip even when
   child buttons individually have conflicting size rules (e.g. .btn-icon's
   fixed 32x32 or the base button padding).
   Mobile touch-target overrides below (min-height: 44px) still win because
   they apply to `min-height`, not `height`, and min-height always trumps. */
.table-actions > button,
.table-actions > a,
.table-actions > [role="button"],
.table-actions > form > button,
.table-actions > form > a,
.table-actions > form > [role="button"] {
    height: 32px !important;
    min-width: 32px;
    padding: 0 0.5rem !important;
    font-size: var(--text-sm);
    flex-shrink: 0;
    box-sizing: border-box;
    width: auto !important;  /* counteract .btn-icon's fixed width: 32px */
}
/* Uniform icon glyph size inside the group */
.table-actions svg,
.table-actions i[data-lucide] {
    width: 15px;
    height: 15px;
}
/* Inline forms wrapping action buttons - collapse wrapper height.
   The .flex / .gap-sm / .items-center selectors below MUST stay
   direct-child (>) - without it the rule cascades to any form deep
   inside any flex layout (auth pages live in <main class="flex
   items-center justify-center">, so a permissive descendant
   selector flattened the entire login form into one inline row).
   td and .table-actions are already narrow enough that descendant
   selection is safe; they stay as-is. */
.table-actions form,
td form,
.flex > form,
.gap-sm > form,
.items-center > form { margin: 0; display: inline-flex; align-items: center; }
.popover-panel form { display: block; }
/* Customer form "Same for billing" checkbox - hides the billing address section
   purely via CSS sibling logic. onsubmit handler copies service values into the
   hidden billing inputs at submit time. */
.customer-form:has(.js-billing-same:checked) .js-billing-section { display: none; }

/* ── Forms ── */
input, select, textarea {
    width: 100%;
    /* Slimmer than the previous 0.25 / 0.75 - keeps the touch target
       comfortable (29-30px tall at the new size) while removing the
       "chunky" vertical air the previous padding produced. */
    padding: 0.3rem 0.65rem;
    /* Fluid: 13px on phone → 15px on wide monitor. Replaces the fixed
       --text-md (15px) that made every form feel oversized at high
       resolution. Tokenized via --text-input. */
    font-size: var(--text-sm);
    /* Weight inherits from <body> (--fw-body 380) - same baseline as
       the rest of the app, no per-control redeclaration. */
    font-family: inherit;
    color: var(--heading);
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: var(--radius-sm);
    transition: border-color var(--transition-base) ease, box-shadow var(--transition-base) ease;
    outline: none;
    line-height: 1.5;
}
input:focus, select:focus, textarea:focus {
    border-color: var(--primary);
    box-shadow: var(--shadow-focus-ring);
}
input:user-invalid, select:user-invalid, textarea:user-invalid {
    border-color: var(--danger);
    box-shadow: 0 0 0 2px var(--danger-light);
}
input[type="checkbox"], input[type="radio"] { width: auto; }
input[type="date"], input[type="time"], input[type="datetime-local"] { padding-top: 0.15rem; padding-bottom: 0.15rem; }
/* Hide the up/down spinner on <input type="number">. Native browser
   validation (reject letters, enforce min/max/step) is kept; only the
   visual spinner controls are removed. Chromium + WebKit use the
   ::-webkit-*-spin-button pseudos; Firefox uses appearance: textfield. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
textarea {
    resize: vertical;
    min-height: 80px;
    /* Auto-grow to fit content as the user types. No JS listeners, no
       manual resize-drag for short multi-line inputs. Chrome/Edge 123+,
       Safari 17.4+; older browsers keep the manual resize behaviour. */
    field-sizing: content;
}
select { cursor: pointer; }

label {
    display: block;
    font-size: var(--text-sm);
    /* Normal-weight labels (was medium 500) - thinner, less shouty.
       The hierarchy still reads: label sits in --heading colour,
       input sits in --heading too but at --fw-light, so the label
       still presents as "what is this field" without the chunky bold.
       Tokenized through --fw-normal so a future weight tweak is one
       edit. */
    font-weight: var(--fw-normal);
    font-variation-settings: 'wght' var(--fw-normal);
    color: var(--heading);
    margin-bottom: 0.25rem;
}
label > input, label > select, label > textarea { margin-top: 0.25rem; }

/* ── Form Grid ── */
/* Mobile-first: single column on phones, auto-fit from 480px+ */
.form-grid {
    display: grid;
    /* Fluid-clamp min track - symmetry-preserving column count.
       Same algorithm shape as the fluid type scale at top of file
       (clamp(floor, rem-anchor + vw, ceiling)).

       The min track interpolates 240px → 360px as the viewport grows
       320px → 1920px. Effect on column count for the two container
       widths that exist in this app:

       Popovers (panel max-width 760px):
         viewport 320px  → min 250px → 1 col (mobile)
         viewport 600px  → min 272px → 2 cols (tablet onward)
         viewport 1280px → min 326px → 2 cols
         viewport 1920px → min 360px (capped) → 2 cols
         → ALWAYS 2 cols on desktop. No "surprise 3rd field" tagging
           onto a pair when the viewport happens to be wide enough.

       Page-level forms (max-width 960px below):
         viewport 1280px → 960/326 = 2.94 → 2 cols
         viewport 1920px → 960/360 = 2.67 → 2 cols
         → 2 cols at typical desktop. 3 cols only kicks in on very
           wide / unconstrained containers (system-admin pages).

       Outer min(100%, ...) lets a track collapse to container width
       when the container is narrower than the clamp floor - preserves
       the 1-col fallback on narrow popovers without a @media query. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, clamp(240px, 14rem + 8vw, 360px)), 1fr));
    /* Tighter than the previous 0.75 / 1rem - pairs with the slimmer
       input padding so the whole form reads as a polished tabular sheet
       rather than an airy data-entry wizard. The 0.5rem row gap matches
       the new label margin-bottom (0.25rem) for visual rhythm. */
    gap: 0.5rem 0.85rem;
    max-width: 960px;
    width: 100%;
}
.form-grid.form-grid-compact { row-gap: 0.4rem; }
.form-grid .full-width { grid-column: 1 / -1; }
.form-grid label { margin-bottom: 0; }
/* Read-only detail grids → responsive chip strip. Each
   <div><strong>Label</strong><p>Value</p></div> cell (raw or via
   <readonly-field>) renders as a compact natural-width chip - micro-label
   over value, .stat-chip lineage but with a body-size value (field data,
   not KPI) - flowing left-to-right and wrapping. Interaction stays LIVE
   here (cells contain GPS / entity links), unlike the disabled-form
   .form-readonly dimming below, which this selector out-specifies. */
.form-grid.form-readonly {
    max-width: none;
    display: flex;
    flex-wrap: wrap;
    column-gap: 2.25rem;
    row-gap: 0.9rem;
    align-items: start;
    pointer-events: auto;
    opacity: 1;
}
.form-grid.form-readonly > div {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}
.form-grid.form-readonly > div > strong {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    font-weight: 500;
}
.form-grid.form-readonly > div > p {
    margin: 0;
    font-weight: 600;
    color: var(--heading);
}
/* Long values (notes, addresses) + in-grid section headers keep their
   own full row in the flex flow. */
.form-grid.form-readonly > .full-width,
.form-grid.form-readonly > h4,
.form-grid.form-readonly > h5 {
    flex-basis: 100%;
}

/* Section headers in forms */
.form-grid h4, .form-grid h5,
form h4, form h5 {
    grid-column: 1 / -1;
    margin: 0.75rem 0 0.25rem;
    padding-bottom: 0.35rem;
    border-bottom: 1px solid var(--border);
    font-size: var(--text-body);
    color: var(--primary);
    letter-spacing: 0.02em;
}
form h4:first-child, form h5:first-child { margin-top: 0; }

/* Checkbox in form grid */
.form-grid label:has(input[type="checkbox"]) {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-direction: row;
    cursor: pointer;
    padding-top: 1.5rem;
}
.form-grid label:has(input[type="checkbox"]) input { margin: 0; }
/* Read-only form display - disables interaction, dims slightly */
.form-readonly { pointer-events: none; opacity: 0.9; }

/* Hours + minutes duration entry: two narrow number inputs with h/m suffixes.
   Used by the Service price-book and Job Template forms so owners enter labor
   time the way they think about it (1h 30m) while storage stays total minutes. */
.duration-hm { display: flex; align-items: center; gap: 0.4rem; }
.duration-hm input { width: 4.5rem; }
.duration-hm .duration-hm-unit { color: var(--text-muted); font-size: var(--text-sm); }

/* Form button bar */
.form-actions {
    display: flex;
    gap: 0.5rem;
    /* Tighter than the previous 1.25 / 1 - matches the form-grid gap
       reduction so the submit row doesn't feel marooned below the fields. */
    margin-top: 0.85rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border);
}

/* ── Bulk Action Bar ──
   Smoothly grows in above the table when rows are selected, instead of the
   prior x-show display-flip that snapped the table down by ~50px (the
   user-reported "glitch"). The bar stays in document flow always, just
   collapses to zero height when inactive - so the natural table header
   position is preserved when the bar is hidden, and the bar slides in
   above it (NOT over it) when activated.

   Mechanism: max-height transition from 0 to a generous cap, combined with
   padding / margin / border collapses to truly zero space when inactive.
   When .is-active: opacity fade-in + tiny translateY for the polish.
   .is-active toggles via Alpine :class - no x-show display-flip, no
   layout snap.

   Dropped the prior position:sticky pin. Sticky-at-viewport meant the bar
   overlapped the (non-sticky) table header once you scrolled - the second
   half of the "glitch over the table" feeling. The bar now sits naturally
   above the table and scrolls away with it. */
.bulk-action-bar {
    /* Collapsed state - zero footprint so layout doesn't shift when toggled. */
    max-height: 0;
    opacity: 0;
    margin-bottom: 0;
    padding-block: 0;
    border-width: 0;
    overflow: hidden;
    transform: translateY(-4px);
    transition: max-height var(--transition-base) ease,
                opacity var(--transition-base) ease,
                transform var(--transition-base) ease,
                margin-bottom var(--transition-base) ease,
                padding var(--transition-base) ease,
                border-width var(--transition-base) ease;

    background: var(--card);
    padding-inline: 1rem;
    border-radius: var(--radius);
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    border-style: solid;
    border-color: var(--primary);
    box-shadow: 0 2px 8px rgba(105, 108, 255, 0.12);
    z-index: var(--z-sticky);
}
.bulk-action-bar.is-active {
    max-height: 12rem;       /* generous cap - 1-2 lines of bar fit easily */
    opacity: 1;
    margin-bottom: 1rem;
    padding-block: 0.5rem;
    border-width: 1px;
    transform: translateY(0);
    overflow: visible;       /* let select dropdowns escape once visible */
}
.bulk-action-bar .bulk-count {
    font-family: var(--font-display);
    color: var(--primary);
    font-size: var(--text-sm);
    font-weight: 600;
    white-space: nowrap;
    padding-right: 0.5rem;
    border-right: 1px solid var(--border);
}
.bulk-action-bar .bulk-field {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}
.bulk-action-bar .bulk-field label {
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
}
.bulk-action-bar select {
    margin: 0;
    padding: 0.3rem 0.5rem;
    font-size: var(--text-sm);
    min-width: 130px;
}
.bulk-action-bar button[type="submit"],
.bulk-action-bar .btn-sm,
.bulk-action-bar a.btn-sm {
    font-size: var(--text-sm);
    padding: 0;
    height: auto;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    padding: 0.3rem 0.6rem;
    box-sizing: border-box;
}
.bulk-action-bar select,
.bulk-action-bar button[type="submit"],
.bulk-action-bar .btn-sm,
.bulk-action-bar a.btn-sm {
    height: 30px;
}
/* Mobile: a clean action panel. Labelled fields (Status/Technician) each take a
   full row; the standalone action buttons (Fan-out / Delete) share a row. Row +
   wrap with explicit per-form widths - the earlier overflow came from a select
   that couldn't shrink (min-width:130px) inside an inline-flex field, now fixed
   by the grid + min-width:0 below. */
@media (width <= 640px) {
    .bulk-action-bar {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: stretch;
        gap: 0.55rem;
        padding-block: 0.7rem;
        /* override an inherited text-align:center so labels/count read as
           left-aligned form fields. */
        text-align: left;
    }
    .bulk-action-bar.is-active { max-height: 34rem; }

    /* "N selected" is a full-width header row with a hairline under it. */
    .bulk-action-bar .bulk-count {
        flex: 1 0 100%;
        border-right: none;
        border-bottom: 1px solid var(--border);
        padding: 0 0 0.5rem;
        font-size: var(--text-base);
    }

    /* A form with a labelled field takes a full row; a standalone-button form
       shares its row with siblings (Fan-out + Delete go 50/50). */
    .bulk-action-bar form:has(.bulk-field) { flex: 1 0 100%; }
    .bulk-action-bar form:not(:has(.bulk-field)) { flex: 1 1 0; min-width: 0; }

    /* Caption label on its own row, then [select fills | action button]. */
    .bulk-action-bar .bulk-field {
        display: grid;
        grid-template-columns: 1fr auto;
        align-items: stretch;   /* select + button share one height */
        column-gap: 0.4rem;
        row-gap: 0.3rem;
    }
    .bulk-action-bar .bulk-field label {
        grid-column: 1 / -1;
        text-transform: uppercase;
        font-size: var(--text-xs);
        letter-spacing: 0.03em;
        font-weight: 600;
    }
    /* height:auto so the select stretches to the button's 40px touch height
       instead of staying at the base 30px (button was taller than the input). */
    .bulk-action-bar .bulk-field select { min-width: 0; width: 100%; height: auto; }

    /* Standalone action buttons fill their (shared) row. */
    .bulk-action-bar form:not(:has(.bulk-field)) > a.btn-sm,
    .bulk-action-bar form:not(:has(.bulk-field)) > button.btn-sm,
    .bulk-action-bar form:not(:has(.bulk-field)) > button[type="submit"] {
        width: 100%;
        justify-content: center;
    }
}

/* ── Key-value table (settings readonly) ── */
.kv-table { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; box-shadow: none; }
/* Was font-size: var(--text-body) (14→16px fluid)
   which read ~1-2px larger than sibling .card-table rows (which inherit
   the default `table` rule's --text-base / 13→14px Linear-Stripe density).
   Aligned to --text-base so a kv-table inside a detail page tracks the
   surrounding card-table density. */
.kv-table td { padding: 0.5rem 0.75rem; border: 1px solid var(--border); vertical-align: top; font-size: var(--text-sm); }
.kv-table td:first-child { color: var(--text-muted); white-space: nowrap; width: 1%; background: var(--bg); }
/* Let long unbreakable values (booking URLs, emails) wrap so a 2-col readonly
   table never forces horizontal scroll on a phone. */
.kv-table td:last-child { color: var(--heading); overflow-wrap: anywhere; }

/* Dispatch Week-grid table - diverges from the default `table` rule
   because the week-grid needs:
     * border-collapse: separate + border-spacing: 0 - so sticky
       header / sticky first-column cells render their shadows
       cleanly (collapsed borders eat the shadow on stuck cells).
     * min-width: 980px - forces horizontal scroll on narrow
       viewports rather than reflow-collapsing the grid.
     * table-layout: fixed - predictable column widths regardless
       of cell content length.
   Promoted from the inline `style="..."` that previously lived on
   the <table> in Features/Dispatch/Views/Week.cshtml. */
.dispatch-week-grid {
    border-collapse: separate;
    border-spacing: 0;
    min-width: 980px;
    table-layout: fixed;
}

/* ── Tables ── */
/* Any container that routinely wraps a table gets horizontal scroll on
   narrow screens. Covers:
     * [id$="-list"]  - the admin list partials (jobs-list, customer-list, etc)
     * [data-tab-panel] - detail-view tabs (Time, Parts, Phases, Signatures...)
     * .card-body     - cards that host standalone tables
   Using :has() instead of wrapping every <table> in its own div avoids
   ~100 template edits. Desktop behaviour unchanged (overflow-x:auto only
   kicks in when content exceeds the container width). */
[id$="-list"]:has(table),
[data-tab-panel]:has(table),
.card-body:has(table) { overflow-x: auto; }
table {
    width: 100%;
    border-collapse: collapse;
    /* Tables consume --text-base (the "default compact body" token), which
       is now a fluid clamp(13→14 px) - same curve as inputs and other
       compact UI. Cell text scales modestly with viewport while staying in
       the dense Linear/Stripe density range. <small>, .text-sm, and topbar
       chrome share the same token so inline subtext markers stay consistent
       with the cell baseline. Font-weight inherits from <body>
       (--fw-body 380) to preserve the polished thin-text look at every
       breakpoint. */
    font-size: var(--text-sm);
    background: var(--card);
    border-radius: var(--radius);
    overflow: hidden;
}
thead th {
    background: var(--card-hover);
    color: var(--heading);
    font-weight: 600;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    /* Compactness pass - cell padding tightened from 12px/16px to 9px/14px.
       Tables now read as scannable lists (Linear / Stripe density) rather
       than airy data grids. Combined with the row-actions-menu in column 0
       and custom-fields-in-accordion, lists feel polished, not crowded. */
    padding: 0.55rem 0.85rem;
    border-bottom: 1px solid var(--border);
    text-align: left;
    line-height: 1.3;
}
tbody td {
    padding: 0.55rem 0.85rem;
    border-bottom: 1px solid var(--border-light);
    vertical-align: middle;
    color: var(--text);
    line-height: 1.4;
    /* Weight inherits from <body> (--fw-body 380) - single source of
       truth across the app. Headers + .fw-500/600/700 cells override
       this where emphasis is genuinely needed. */
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: rgba(var(--primary-rgb), 0.04); }

/* ── Table footer (totals row) ── */
tfoot td, tfoot th {
    padding: 0.75rem 1rem;
    /* Totals row - inherits everything else from <body> (size, family,
       colour fallback) but opts up to the tokenized --fw-semibold
       (calibrated 540) so summed values read as the table's anchor.
       This is the "totals should inherit but be bolded" pattern: no
       per-cell .fw-500 needed in the markup; the semantic <tfoot>
       wrapper is the cue. */
    font-weight: var(--fw-semibold);
    font-variation-settings: 'wght' var(--fw-semibold);
    color: var(--heading);
    background: var(--bg);
    border-top: 2px solid var(--border);
    border-bottom: none;
    font-size: var(--text-body);
}
tfoot tr:first-child td, tfoot tr:first-child th { border-top: 2px solid var(--border); }
tfoot tr + tr td, tfoot tr + tr th { border-top: 1px solid var(--border-light); }

/* ── Badges ── */
/* Compact-by-default per design preference. Was 0.2rem 0.7rem with
   line-height 1.5 + text-sm font; tightened so badges read as labels
   nested in a row, not standalone pills. The .badge-xs / .badge-sm
   modifiers still scale down further for ultra-dense surfaces. */
.badge {
    font-family: var(--font-display);
    display: inline-block;
    padding: 0.1rem 0.45rem;
    border-radius: var(--radius-lg);
    font-size: var(--text-xs);
    font-weight: 600;
    line-height: 1.3;
}
.badge.badge-xs { font-size: var(--step--2); padding: 0.05rem 0.35rem; }
.badge.badge-sm { font-size: var(--step--1); padding: 0.1rem 0.45rem; }

/* Inline status editor - a <select> styled to look like a .badge so a
   detail page header can show "current status" and "change status" as
   one control instead of stacking a badge + a separate Change Status
   card. Single CSS rule, single source of truth - the chevron is a
   ::after pseudo on the WRAPPER (not the select itself, because <select>
   doesn't accept pseudo-elements). The pseudo's mask-image carries the
   chevron SHAPE only, and `background-color: currentColor` colors it
   from the wrapper's text colour (which any .badge-* class supplies).
   That sidesteps the data:URL SVG / currentColor inheritance quirk and
   collapses the per-colour overrides we used to need into one rule.

   Markup:
       <span class="status-select-wrap badge-{status}">
           <select class="status-select" ...>...</select>
       </span>

   The .badge-* class moves from the <select> to the wrapper so its
   `color: var(--xxx)` propagates to the pseudo via currentColor. */
.status-select-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    border-radius: var(--radius-lg);
    /* The .badge-* class on this element supplies bg + color via its
       existing `background: var(--xxx-light); color: var(--xxx);` rule. */
}
.status-select-wrap > select.status-select {
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.5;
    /* Right padding leaves room for the absolutely-positioned chevron
       so text never overlaps the indicator. */
    padding: 0.2rem 1.5rem 0.2rem 0.7rem;
    border-radius: var(--radius-lg);
    border: 1px solid transparent;
    background: transparent;             /* wrapper supplies the badge tint */
    color: inherit;                      /* picks up wrapper's badge color */
    cursor: pointer;
    width: auto;
    height: auto;
    margin: 0;
    appearance: none;
}
.status-select-wrap > select.status-select:focus {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}
.status-select-wrap:hover { filter: brightness(0.97); }
.status-select-wrap::after {
    content: "";
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    width: 0.9rem;
    height: 0.9rem;
    pointer-events: none;                /* clicks pass through to the select */
    /* mask gives the chevron its SHAPE; background-color paints the colour.
       currentColor on a pseudo-element DOES inherit from the wrapper, so
       whatever colour the .badge-* class set on the wrapper becomes the
       chevron colour automatically - no per-variant rules, no hex hardcode,
       no theme-mode overrides. */
    background-color: currentColor;
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
    mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

/* Vertical Packs tab - choose-one grid of trade packs. Each card is itself
   the Apply form (the whole card is the action surface), with concrete
   preview counts so the admin sees what they're about to add. Simple,
   accessible (semantic <dl>), intuitive (one CTA per card). */
/* Pack counts grid - used inside <info-card> on the Vertical Packs admin tab.
   Card chrome itself comes from .info-card; only the inner <dl> needs custom layout. */
.vertical-pack-counts {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.35rem 0.75rem;
    margin: 0;
    font-size: var(--text-sm);
}
.vertical-pack-counts > div { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.vertical-pack-counts dt { color: var(--text-muted); margin: 0; }
.vertical-pack-counts dd { margin: 0; font-weight: 600; color: var(--heading); }

/* Service / Price-Book item form - fieldset grouping for the three
   logical sections (Identity / Pricing / Operational). Native <fieldset>
   gives screen readers proper grouping; this rule strips the default
   browser border so it visually fits the flat card design. */
.form-fieldset { border: 0; padding: 0; margin: 0 0 1.25rem; }
.form-fieldset legend {
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0;
    margin-bottom: 0.5rem;
}

/* Definition-list grid - used on Service detail "Specs" section and
   anywhere a key:value layout reads better than a flat table. dt is
   the muted label, dd is the prominent value. */
.kv-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
    gap: 0.5rem 1rem;
    margin: 0;
}
.kv-grid > div { display: flex; flex-direction: column; gap: 0.15rem; }
.kv-grid > div.full-width { grid-column: 1 / -1; }
.kv-grid dt { font-size: var(--text-xs); color: var(--text-muted); margin: 0; text-transform: uppercase; letter-spacing: 0.04em; }
.kv-grid dd { margin: 0; font-weight: 500; color: var(--heading); }

/* Hierarchical category browser - generic <details>-based tree.
   Used today by the Price-Book index + picker; reusable by any future
   feature that needs a "Category > Subcategory > leaf" sidebar. Owns
   its own scroll so a 200-item catalog doesn't push the page down. */
.cat-tree {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.4rem;
    max-height: calc(100dvh - 12rem);
    overflow-y: auto;
    font-size: var(--text-sm);
}
.cat-tree a {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.6rem;
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
    transition: background-color 0.1s;
}
.cat-tree a:hover { background: var(--bg); color: var(--heading); }
/* !important beats the dark-mode link rule (paints every <a> --primary); without
   it the active label is --primary text on the --primary-light wash = invisible. */
.cat-tree a.is-active { background: var(--primary-light); color: var(--heading) !important; font-weight: 600; }
.cat-tree__count {
    margin-left: auto;
    font-size: var(--text-xs);
    color: var(--text-muted);
    background: var(--bg);
    padding: 0.05rem 0.4rem;
    border-radius: var(--radius-pill);
}
.cat-tree a.is-active .cat-tree__count { background: var(--card); color: var(--primary); }
.cat-tree__all { font-weight: 600; border-bottom: 1px solid var(--border-light); margin-bottom: 0.25rem; padding-bottom: 0.4rem; }
.cat-tree__leaf { /* category with no subcategories */ padding-left: 1.25rem; }

/* Native <details> categories inside .cat-tree - summary expands without
   JS. Hide the UA marker; draw our own chevron so it tracks text color
   in both themes. */
.cat-tree details { margin: 0; }
.cat-tree details > summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 0.25rem;
}
.cat-tree details > summary::-webkit-details-marker { display: none; }
.cat-tree details > summary::before {
    content: "";
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    margin-left: 0.4rem;
    opacity: 0.6;
    transition: transform 0.15s;
}
.cat-tree details[open] > summary::before { transform: rotate(90deg); }
.cat-tree details > summary > a { flex: 1; padding-left: 0.25rem; }
.cat-tree details > ul { list-style: none; padding: 0; margin: 0 0 0.25rem 1.5rem; }
.cat-tree details > ul a { font-size: var(--text-sm); color: var(--text-muted); }

/* ══════════════════════════════════════════════════════════════
   Price-Book picker v2 - searchable combobox
   ══════════════════════════════════════════════════════════════
   Single column, search-first. No category tree, no per-row tier
   chips, no per-row qty input. The user types to find a service
   (search hits Name + Category + Subcategory + Description so
   typing "hvac" or "ac" works); the row click commits the
   service at the customer-resolved tier with qty=1. Tier and
   qty overrides happen on the line item itself in the parent.

   Layout pieces:
     .picker-v2                  - outer wrapper
     .picker-v2-title            - "Price Book" header row
     .picker-v2-search           - large search input
     .picker-v2-context          - small pricing-tier + BOM-toggle row
     .picker-v2-list             - scrollable result list
       .picker-v2-section-header - "Recently added" / "All services"
       .picker-v2-row            - one service (button - full click target)
         .picker-v2-thumb        - 40x40 image or package placeholder
         .picker-v2-body         - name + meta column
         .picker-v2-price        - right-aligned price chip
     .picker-v2-empty            - "no matches" empty state
   ══════════════════════════════════════════════════════════════ */
/* Picker-v2 outer container. Fixed viewport-relative height so the
   popover doesn't snap from "big list" to "tiny list" as the
   client-side filter trims results. clamp keeps it usable on short
   viewports (mobile portrait) and capped on tall ones (desktop). */
.picker-v2 {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    height: clamp(420px, 78vh, 720px);
    width: clamp(360px, 92vw, 720px);
    max-width: 100%;
}
/* Filter-hide helper - scoped so it doesn't collide with anything
   else that wants display:none semantics. */
.picker-v2 .is-hidden { display: none !important; }
.picker-v2-search {
    width: 100%;
    padding: 0.65rem 0.85rem;
    font-size: var(--text-body);
    line-height: 1.3;
}

/* Pricing-tier indicator + BOM toggle. Sits between the search input
   and the result list as a quiet context line. Strong on the tier
   word so the user can scan it ("Pricing: Member") without parsing. */
.picker-v2-context {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.4rem 0.65rem;
    background: var(--bg);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    color: var(--text);
}
.picker-v2-context strong { color: var(--heading); }
.picker-v2-context input[type="checkbox"] { margin: 0; }

/* Result list. Scrolls within the popover; height is sized so the
   search + context above stay visible without scrolling.

   Note: deliberately NO loading indicator on the list. On localhost
   the search response settles in 5-10ms and any animated indicator
   (whole-list opacity fade, top-edge progress bar, etc.) ends up
   flickering on/off with every keystroke instead of communicating
   anything. If a slow-network indicator is wanted later, attach it
   to the search input itself (spinner-in-the-search-icon pattern)
   with a `htmx-request` delay so it only appears for requests that
   actually take long enough to need feedback. */
.picker-v2-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    flex: 1;
    min-height: 0;  /* allow flex to shrink the list inside the fixed-height parent */
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--card);
}

/* Section header inside the list - "Recently added" / "All services".
   --secondary modifier mutes the second header so the visual weight
   stays on the recents pin (the implicit "act here first" cue). */
.picker-v2-section-header {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.85rem 0.35rem;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--primary);
    /* Mix with --card (not transparent) so the sticky header always
       reads opaque - preserves the subtle primary tint while making
       the swap-time visuals predictable (no translucent stacking). */
    background: color-mix(in srgb, var(--primary) 6%, var(--card));
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 1;
}
.picker-v2-section-header--secondary {
    color: var(--text-muted);
    background: var(--bg);
    margin-top: 0.4rem;
}
.picker-v2-section-header i,
.picker-v2-section-header svg { width: 13px; height: 13px; }

/* Row = full clickable surface. Each row is a real <button> so
   keyboard nav (Tab + Enter) works without extra wiring; the base
   button rule's not-list (.picker-v2-row) lets us style it as a
   list row instead of a primary pill. */
.picker-v2-row {
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.75rem;
    padding: 0.55rem 0.85rem;
    background: transparent;
    border: none;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    cursor: pointer;
    text-align: left;
    width: 100%;
    transition: background-color 0.08s;
    font-family: inherit;
    color: var(--text);
}
.picker-v2-row:last-child { border-bottom: none; }
.picker-v2-row:hover,
.picker-v2-row:focus-visible {
    background: color-mix(in srgb, var(--primary) 6%, var(--card));
    outline: none;
}
.picker-v2-row:active {
    background: color-mix(in srgb, var(--primary) 14%, var(--card));
}

/* Thumb: real img + placeholder share the 36x36 box. */
.picker-v2-thumb {
    width: 36px;
    height: 36px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--bg);
    border: 1px solid var(--border);
    flex: 0 0 36px;
}
/* CSS-only placeholder for services without an image. A muted
   diagonal-stripe pattern reads as "empty image slot" without
   relying on a JS-replaced lucide icon (which produced a 1-frame
   pop-in flash with each list re-render during search). */
.picker-v2-thumb--placeholder {
    background:
        repeating-linear-gradient(
            45deg,
            color-mix(in srgb, var(--text-muted) 8%, var(--bg)) 0 4px,
            var(--bg) 4px 8px
        );
}

/* Body column: name (2-line clamp) + meta (single-line). */
.picker-v2-body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}
.picker-v2-name {
    font-weight: 600;
    color: var(--heading);
    font-size: var(--text-sm);
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.picker-v2-meta {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1.3;
}

/* Price chip on the right - primary-tinted background so the row's
   commit affordance reads as the "click target" before the user
   parses the row content. Hover state on the row lifts the chip
   to solid primary so the click feedback is unmissable. */
.picker-v2-price {
    flex: 0 0 auto;
    padding: 0.3rem 0.7rem;
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    color: var(--primary);
    font-weight: 600;
    font-size: var(--text-sm);
    border-radius: var(--radius-pill);
    transition: background-color 0.08s, color 0.08s;
    white-space: nowrap;
}
.picker-v2-row:hover .picker-v2-price,
.picker-v2-row:focus-visible .picker-v2-price {
    background: var(--primary);
    color: #fff;
}

/* Empty state - search returned no matches OR no recents yet. */
.picker-v2-empty {
    text-align: center;
    padding: 2.5rem 1rem;
    color: var(--text-muted);
}
.picker-v2-empty i,
.picker-v2-empty svg {
    width: 36px;
    height: 36px;
    margin-bottom: 0.6rem;
    opacity: 0.6;
}
.picker-v2-empty p { margin: 0.2rem 0; }

/* Narrow viewport - row stays single-line; price chip just keeps its
   slot on the right since both columns shrink to fit. */
@media (width <= 480px) {
    .picker-v2-row { padding: 0.5rem 0.6rem; gap: 0.55rem; }
}

/* ── Picker multi-select (line-item hosts) ──────────────────────────
   Each row is a <label> with a leading checkbox; tick any number,
   then the footer commit bar adds them all in one request. */
.picker-v2-row--check {
    grid-template-columns: 22px 36px minmax(0, 1fr) auto;
}
.picker-v2-check {
    width: 17px;
    height: 17px;
    margin: 0;
    cursor: pointer;
    accent-color: var(--primary);
}
.picker-v2-row.is-selected {
    background: color-mix(in srgb, var(--primary) 12%, var(--card));
}
.picker-v2-row.is-selected .picker-v2-price {
    background: var(--primary);
    color: #fff;
}
/* Commit bar pinned below the scrolling list. */
.picker-v2-footer {
    flex: 0 0 auto;
}
.picker-v2-add {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
}
.picker-v2-add[disabled] {
    opacity: 0.55;
    cursor: not-allowed;
}

/* ── Line-items table - type grouping ───────────────────────────────
   Services / Parts / Labor / Other render as labelled groups with a
   per-group subtotal, so a mixed billable list reads as distinct
   sections instead of one interleaved run of rows. */
.li-group-head td {
    font-weight: 600;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--primary);
    background: color-mix(in srgb, var(--primary) 6%, var(--card));
}
.li-group-subtotal td {
    color: var(--text-muted);
    font-size: var(--text-sm);
    border-top: 1px solid var(--border);
}

/* ══════════════════════════════════════════════════════════════
   Sectioned forms - <form-sections> / <form-section>
   A long multi-section form becomes a horizontal scroll-snap track:
   one section visible at a time, swipe on touch + dot/arrow nav on
   desktop (built by initFormSections() in app.js). Stays one <form>
   - the Save button lives in .form-actions OUTSIDE the track.
   ══════════════════════════════════════════════════════════════ */
.form-sections-track {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;            /* Firefox - nav is the affordance */
    overscroll-behavior-x: contain;
}
.form-sections-track::-webkit-scrollbar { display: none; }   /* WebKit */

.form-section {
    flex: 0 0 100%;
    min-width: 0;
    scroll-snap-align: start;
    /* a hair of inline padding so focus rings near the snap edge
       aren't visually clipped by the overflow */
    padding: 0.15rem 0.2rem 0.25rem;
}
.form-section-head {
    color: var(--heading);
    margin: 0 0 0.85rem;
}

/* Nav row - ‹  • • •  › - appended after the track by the JS module. */
.form-sections-nav {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.85rem;
    margin-top: 0.6rem;
}
.form-sections-dots {
    display: flex;
    align-items: center;
    gap: 0.45rem;
}
.form-sections-dot {
    width: 8px;
    height: 8px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--border);
    cursor: pointer;
    transition: background-color 0.15s, transform 0.15s;
}
.form-sections-dot:hover { background: var(--text-muted); }
.form-sections-dot.is-active {
    background: var(--primary);
    transform: scale(1.3);
}
.form-sections-arrow {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--card);
    color: var(--heading);
    cursor: pointer;
    transition: opacity 0.15s, border-color 0.15s, color 0.15s;
}
.form-sections-arrow:hover:not([disabled]) {
    border-color: var(--primary);
    color: var(--primary);
}
.form-sections-arrow[disabled] {
    opacity: 0.3;
    cursor: default;
}

/* Job Detail lifecycle summary. The bar that
   reads "Created → Arrived → Completed → Invoiced" used an inline
   style override defeating --pad-card-dense; class below uses the
   token directly and tightens the inter-step gap to 1.25rem (gap-xl
   was 2rem, too much for a 4-step linear flow). */
.lifecycle-summary { padding: var(--pad-card-dense, 1.25rem); }
.lifecycle-summary > div { gap: 1.25rem; }

/* Job Detail Scope tab rhythm. Long-form
   Description + DiagnosisNotes wear `<p class="scope-prose">` so a
   max-width caps the line length at the conventional comfortable
   reading width. Without this they river the full width of the
   right panel at widescreen breakpoints, killing read-comfort. The
   h4 margin-top adds breathing room between Scope's optional sections
   so they don't crash into each other when several are populated. */
.scope-prose { max-width: 70ch; }
#tab-details h4 { margin-top: 1.25rem; }
#tab-details h4:first-of-type { margin-top: 0.5rem; }

/* Tablet/mobile shell for the Price Book.
   Between 640px and 1024px the two-pane shell (180-220px tree +
   table) squeezes the 8-column price-book list table so the
   Commercial column gets cropped. Below 1024px we collapse the
   shell to single-column: the tree becomes a horizontally-scrolling
   chip strip above the list, freeing full viewport width for the
   price columns. The shell layout lives on the Index view's inline
   grid-template-columns; the `.services-shell` class below scopes
   the override so it can be flipped via a single media query
   without touching the Razor markup further. */
@media (width <= 1024px) {
    .services-shell { grid-template-columns: 1fr !important; }
    .services-shell .cat-tree {
        display: flex;
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: 0.25rem;
        padding-bottom: 0.25rem;
        margin-bottom: 1rem;
        border-bottom: 1px solid var(--border);
    }
    .services-shell .cat-tree > * { flex: 0 0 auto; }
    .services-shell .cat-tree details { display: contents; }
    .services-shell .cat-tree details > summary { flex: 0 0 auto; }
    .services-shell .cat-tree details > ul {
        display: none; /* drop subcategories on narrow - pick the category from the chip strip, refine with the search input */
    }
}

/* Capacity scheduler - slot suggestion cards on the Job Create form.
   Each card is itself a <button> so the whole rectangle is the click
   target. Three rows: tech header (avatar + name + score chip), bold
   date/time, drive caption. Selected state lifts the outline + tint. */
/* Capacity slot suggestions - a compact, scroll-contained picker. One
   tight row per slot (avatar / tech + time / drive chip); the list is
   height-capped so a 10-row result never blows up the popover it lives
   in. The internal score isn't shown - the row order already conveys
   the ranking; drive time is the signal a dispatcher actually reads. */
.slot-list {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin-top: 0.6rem;
    max-height: 15rem;
    overflow-y: auto;
}
.slot-suggestions-empty {
    padding: 1rem;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--text-sm);
}
.slot-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    text-align: left;
    padding: 0.45rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--card);
    color: inherit;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
}
.slot-row:hover { border-color: var(--primary); background: var(--primary-light); }
.slot-row:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
/* Selected state - set programmatically by applySlotSuggestion in app.js.
   A dedicated class keeps the visual contract in CSS so dark mode picks
   it up automatically. */
.slot-row.is-selected { border-color: var(--primary); background: var(--primary-light); }
/* Display-only (Service Request detail) - no job to apply into yet. */
.slot-row--display-only { cursor: default; }
.slot-row--display-only:hover { border-color: var(--border); background: var(--card); }
.slot-row__avatar { width: 28px; height: 28px; font-size: var(--text-xs); flex-shrink: 0; }
.slot-row__main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.slot-row__tech {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--heading);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.slot-row__when { font-size: var(--text-xs); color: var(--text-muted); }
.slot-row__drive {
    flex-shrink: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
    background: var(--bg);
    padding: 0.15rem 0.5rem;
    border-radius: var(--radius-pill);
}

/* @mention autocomplete menu for note inputs - a manual popover so it
   renders in the top layer above the note's own popover panel. JS sets
   left/top from the textarea's bounding rect. */
.mention-menu {
    position: fixed;
    margin: 0;
    inset: auto;
    max-width: 20rem;
    max-height: 12rem;
    overflow-y: auto;
    padding: 0.25rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.mention-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.6rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--text-sm);
}
.mention-item.is-active { background: var(--primary-light); }
.mention-item__name { color: var(--heading); white-space: nowrap; }
.mention-item__role {
    margin-left: auto;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* Rendered mention chip - server-side @[Name](user-id) tokens become
   one of these in the note feed. Pill-shape signals "this is a tagged
   person," not just bolded text. */
.mention-chip {
    display: inline-block;
    padding: 0.05rem 0.4rem;
    border-radius: var(--radius-pill);
    background: var(--primary-light);
    color: var(--primary);
    font-weight: 500;
    font-size: 0.95em;
    text-decoration: none;
}
/* Mentions are links (open a DM with the user) - keep the chip look,
   signal interactivity on hover instead of underlining. */
a.mention-chip:hover {
    background: var(--primary);
    color: var(--white);
    text-decoration: none;
}

/* ── Note composer (contenteditable @mention input) ──
   A contenteditable div styled to read like the textarea it replaced, so a
   picked mention can render as an atomic chip inline while typing. */
.js-note-input[contenteditable] {
    display: block;
    width: 100%;
    min-height: 4.5rem;
    max-height: 14rem;
    overflow-y: auto;
    padding: 0.5rem 0.65rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--input-bg, var(--card));
    color: var(--text);
    font: inherit;
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    cursor: text;
}
.js-note-input[contenteditable].flex-1 { width: auto; min-height: 2.75rem; }
.js-note-input[contenteditable]:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-light);
}
/* Placeholder - a contenteditable isn't reliably :empty (the browser leaves
   a stray <br>), so app.js toggles .is-empty on serialized emptiness. */
.js-note-input[contenteditable].is-empty::before {
    content: attr(data-placeholder);
    color: var(--text-muted);
    pointer-events: none;
}
/* In-composer mention chip: atomic (contenteditable=false) pill with a small
   remove button. The non-editable span deletes as one unit on backspace. */
.mention-chip--editable {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    padding: 0.05rem 0.2rem 0.05rem 0.45rem;
    user-select: none;
    white-space: nowrap;
    vertical-align: baseline;
}
.mention-chip-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.05em;
    height: 1.05em;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 0.9em;
    line-height: 1;
    cursor: pointer;
    opacity: 0.7;
}
.mention-chip-x:hover { background: var(--primary); color: var(--white); opacity: 1; }

/* Disclosure summary as pill badge.
   The .multi-select-trigger exclusion keeps the <multi-select> Tag Helper
   summary out of this pill theme; it has its own input-styled chrome
   (see .multi-select-trigger rules). Any future "<summary> that isn't a
   pill" pattern adds its class here too. */
summary:not(.multi-select-trigger) {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.3rem 0.85rem;
    border-radius: var(--radius-lg);
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--primary);
    background: var(--primary-light);
    cursor: pointer;
    list-style: none;
    transition: var(--transition-btn);
}
summary:not(.multi-select-trigger)::-webkit-details-marker { display: none; }
summary:not(.multi-select-trigger)::marker { display: none; content: ''; }
summary:not(.multi-select-trigger)::after { content: '+'; font-weight: 700; font-size: 0.9em; }
summary:not(.multi-select-trigger):hover { background: var(--primary); color: var(--white); }
details[open] > summary:not(.multi-select-trigger) { background: var(--primary); color: var(--white); }
details[open] > summary:not(.multi-select-trigger)::after { content: '\2212'; } /* minus sign when open */
/* Collapsible "fine-tune / advanced" group inside a form: keeps secondary
   knobs out of the way so the form reads short, expands on demand. */
.form-advanced { margin: 0.85rem 0 0; }
.form-advanced > .form-grid { margin-top: 0.85rem; }
/* The eval generator sits on a wide system-admin card. The default 960px form
   cap holds it to two columns there, orphaning the third field; lift the cap so
   the same auto-fit packs three fields inline when the card is wide and still
   collapses to one column on a phone. The auto-fit min track caps column count
   at three across any desktop width, so this never sprawls. */
#eval-generate-form .form-grid { max-width: 1140px; }

.badge-draft, .badge-inactive, .badge-proposed, .badge-filed, .badge-closed { background: var(--border-light); color: var(--text); }
.badge-new, .badge-sent, .badge-assigned, .badge-applied, .badge-submitted, .badge-maintenance, .badge-reviewed, .badge-medium, .badge-normal { background: var(--info-light); color: var(--info); }
.badge-approved, .badge-active, .badge-completed, .badge-paid, .badge-reimbursed, .badge-refunded, .badge-synced, .badge-received, .badge-confirmed { background: var(--success-light); color: var(--success); }
.badge-rejected, .badge-overdue, .badge-cancelled, .badge-lost, .badge-denied, .badge-failed, .badge-retired, .badge-urgent { background: var(--danger-light); color: var(--danger); }
.badge-inprogress, .badge-contacted, .badge-qualified, .badge-proposal, .badge-partial, .badge-expiring { background: var(--warning-light); color: var(--warning); }
/* Extended status badges - use badge token variables for dark mode compatibility */
.badge-scheduled { background: var(--badge-purple-bg); color: var(--badge-purple-fg); }
.badge-enroute, .badge-underreview, .badge-inreview { background: var(--badge-info-bg); color: var(--badge-info-fg); }
.badge-onhold, .badge-paused, .badge-expired, .badge-abandoned, .badge-writtenoff, .badge-void { background: var(--badge-muted-bg); color: var(--badge-muted-fg); }
.badge-followup { background: var(--badge-orange-bg); color: var(--badge-orange-fg); }
.badge-won, .badge-accepted { background: var(--badge-success-bg); color: var(--badge-success-fg); }
.badge-low { background: var(--border-light); color: var(--text); }
.badge-pending, .badge-rescheduled { background: var(--badge-primary-bg); color: var(--badge-primary-fg); }
.badge-renewaldue, .badge-flagged, .badge-partiallyapproved, .badge-revisionrequested, .badge-disputed, .badge-needsinfo { background: var(--badge-warning-bg); color: var(--badge-warning-fg); }
.badge-suspended, .badge-noshow, .badge-escalated, .badge-outofservice, .badge-disqualified, .badge-declined, .badge-collections { background: var(--badge-danger-bg); color: var(--badge-danger-fg); }
/* Generic semantic badge tones - used by views that want a tonal pill
   without a specific status meaning (KB categories, custom-field type
   chips, "Required" markers, etc). Mapped to the same dark-mode-aware
   --badge-*-bg/fg tokens as the status-specific aliases above so they
   all stay in lockstep. */
.badge-info { background: var(--badge-info-bg); color: var(--badge-info-fg); }
.badge-warning { background: var(--badge-warning-bg); color: var(--badge-warning-fg); }
.badge-success { background: var(--badge-success-bg); color: var(--badge-success-fg); }
.badge-danger { background: var(--badge-danger-bg); color: var(--badge-danger-fg); }
.badge-secondary { background: var(--badge-muted-bg); color: var(--badge-muted-fg); }

/* ── Tabs ── */
.tabs {
    display: flex;
    gap: 0;
    border-bottom: 2px solid var(--border);
    margin-bottom: 1.5rem;
}
.tabs a, .tabs .tab-btn {
    font-family: var(--font-display);
    padding: 0.6rem 1.25rem;
    text-decoration: none;
    color: var(--text);
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    cursor: pointer;
    font-size: var(--text-body);
    font-weight: 500;
    transition: var(--transition-btn);
    background: transparent;
}
.tabs a:hover, .tabs .tab-btn:hover { color: var(--primary); text-decoration: none; }
.tabs a.active, .tabs .tab-btn.active {
    color: var(--primary);
    border-bottom-color: var(--primary);
    font-weight: 600;
}

/* Vertical tab variant - used on detail pages */
.tabs-vertical {
    flex-direction: column;
    border-bottom: none;
    border-right: 1px solid var(--border);
    position: sticky;
    top: 80px;
    align-self: flex-start;
    /* Width scales fluidly from 180px on desktop down to 48px floor.
       The icon-only visual mode at small viewports is a discrete layout
       switch handled by the @media (width <= 480px) block below. */
    width: clamp(48px, 12vw, 180px);
    gap: 2px;
    padding: 0.5rem 0;
    margin-bottom: 0;
    flex-shrink: 0;
}
.tabs-vertical a, .tabs-vertical .tab-btn {
    border-bottom: none !important;
    border-left: 3px solid transparent;
    margin-bottom: 0;
    padding: 0.35rem 0.6rem;
    font-size: var(--text-sm);
    /* Was white-space: nowrap with no overflow rule,
       so long labels like "Message Templates" / "Outbound Webhooks" clipped
       behind the .tab-count badge at tablet widths (12vw = 92px at 768px).
       text-overflow: ellipsis lets the label fade gracefully when the
       sidebar is narrow but desktop still has room. min-width: 0 lets
       flexbox actually shrink the anchor for ellipsis to engage. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    display: flex;
    align-items: center;
    gap: 0.35rem;
}
.tabs-vertical a:hover, .tabs-vertical .tab-btn:hover {
    background: var(--bg);
}
.tabs-vertical a.active, .tabs-vertical .tab-btn.active {
    border-bottom-color: transparent !important;
    border-left-color: var(--primary);
    background: var(--primary-light);
    color: var(--primary);
    font-weight: 600;
}
.tabs-vertical a i, .tabs-vertical .tab-btn i,
.tabs-vertical a svg, .tabs-vertical .tab-btn svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
}
/* Count indicator - small pill next to the label on desktop; stacked under
   the icon when the tab collapses to icon-only on mobile. */
.tabs-vertical .tab-count {
    margin-left: auto;
    font-size: var(--text-xs);
    font-weight: 600;
    padding: 0 0.35rem;
    border-radius: var(--radius-pill);
    background: var(--bg);
    color: var(--text-muted);
    line-height: 1.35;
    min-width: 1.1rem;
    text-align: center;
}
.tabs-vertical a.active .tab-count, .tabs-vertical .tab-btn.active .tab-count {
    background: var(--primary);
    color: var(--white);
}
/* Width scaling is handled by width: clamp(...) on the base rule.
   At <=768px, the 768px media block switches tabs to icon-only (labels hidden,
   centered icons) so the narrow vertical strip stays readable. */
.detail-layout {
    display: flex;
    gap: 1.5rem;
    min-width: 0;
    /* Detail pages read at table-cell size (--text-base 13px fixed) so
       the dense per-record information feels consistent with the list
       view the user just clicked through from. The body fluid clamp
       (14→17px) was too generous on wide monitors - detail panels
       have lots of label-value pairs, sub-tables, and quick-view
       sections, all of which benefit from the same scannable compact
       size that table cells use. Mobile (<480px) was already fine
       because the body clamp bottoms out near 14px there; the fixed
       --text-base just locks that baseline so it doesn't grow on
       desktop. Long-read surfaces inside a detail page (.article
       with --text-md) still opt up for sustained reading. */
    font-size: var(--text-sm);
}
/* Heading scale INSIDE .detail-layout. Body is pinned at --text-base
   (13px fixed - that's the "compact, table-cell scannable" intent set
   above; do NOT make body fluid here), so the global heading tokens
   (h3 = step-3 18-22px, h4 = step-2 16-19px) feel disproportionately
   large against the compact body - h4 "Description" / "Financials"
   reads almost as big as the page-header h2.
   Step h3 + h4 down to fluid clamp tokens that bottom-out close to body
   on mobile and grow proportionally on desktop. Both tokens are
   clamp()-based (not fixed-rem) so the hierarchy fluidly adapts:
     body     13px  fixed  (--text-base)
     h5       14-18 fluid  (--step-1, unchanged - already proportional)
     h4       14-18 fluid  (--step-1 - was step-2)
     h3       16-22 fluid  (--text-lg - was step-3)
   h4 collapsing to h5's token is intentional: nested h4/h5 inside a
   single detail panel is rare, so reusing the same scale step is cleaner
   than introducing a new in-between token. */
.detail-layout h3, .detail-page h3 { font-size: var(--step-2); }
.detail-layout h4, .detail-page h4 { font-size: var(--step-1); }
/* Single-column detail pages (Warranties, ChangeOrders, CreditMemos, etc.)
   don't use the .detail-layout flex wrapper but want the same compact body
   + heading scale. They opt in via `ViewData["IsDetailPage"] = true;`,
   which the layout reads to add `class="detail-page"` to #content-area. */
.detail-page { font-size: var(--text-sm); }

/* ── Section accordion ──
   Browser-native disclosure for inline detail-page sections. Wrap each
   collapsible section in:
       <details class="section-accordion" open>
           <summary><h4><i data-lucide="..."></i> Title (count)</h4></summary>
           ...action buttons + content...
       </details>

   Native <details>/<summary> handles toggle, keyboard (Enter/Space), and
   ARIA semantics for free. The `open` attribute persists in the rendered
   HTML, so HTMX swaps that target the body don't lose the user's
   expanded/collapsed state.

   The global `summary { ... }` rule earlier in this file styles disclosure
   summaries as primary-tinted pills (used for category trees, KB toggles,
   etc.). This .section-accordion variant resets that treatment so the
   summary reads as a section heading row instead - cleaner against a card
   surface where multiple sections stack.

   The chevron uses background-color: currentColor + mask-image so it
   inherits the surrounding text color (light/dark theme aware) without a
   per-mode override.
*/
.section-accordion { margin: 0; padding: 0; }
/* Selectors below use `details.section-accordion` (not bare `.section-accordion`)
   so they outrank the global `details[open] > summary:not(.multi-select-trigger)`
   pill rule, which carries (0,2,2) specificity from its [open] attribute +
   :not() argument. Without the `details` prefix, `.section-accordion[open] >
   summary` only weighs (0,2,1) and loses; leaving an opened section
   accordion painted in --primary blue with white text. */
details.section-accordion > summary {
    /* Override the global pill-summary treatment for this scoped variant. */
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0;
    margin-bottom: 1rem;
    background: transparent;
    border-radius: 0;
    color: inherit;
    font-size: inherit;
    font-weight: inherit;
    cursor: pointer;
    user-select: none;
    list-style: none;
    transition: color 150ms ease;
}
details.section-accordion > summary:hover {
    background: transparent;
    color: var(--primary);
}
details.section-accordion[open] > summary {
    background: transparent;
    color: inherit;
}
details.section-accordion > summary::-webkit-details-marker { display: none; }
details.section-accordion > summary::marker { content: ''; }
/* Chevron lives on the RIGHT (::after), so the title sits flush-left and
   aligns with non-accordion section headers - no visual indent from a
   leading marker. Override the global summary::after that draws +/− glyphs
   here too - this rule is more specific so it wins. */
details.section-accordion > summary::after {
    content: '';
    width: 14px;
    height: 14px;
    margin-left: auto;     /* pushes chevron to the far right of the row */
    flex-shrink: 0;
    background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='9 18 15 12 9 6'/></svg>") no-repeat center / contain;
    mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='9 18 15 12 9 6'/></svg>") no-repeat center / contain;
    transition: transform 200ms ease;
    /* Closed: chevron points down (▾) hinting "expand to see more".
       Open: rotates 180° to point up (▴) hinting "click to collapse". */
    transform: rotate(90deg);
}
details.section-accordion[open] > summary::after { transform: rotate(-90deg); }
/* Heading inside summary takes the rest of the row. */
details.section-accordion > summary > h3,
details.section-accordion > summary > h4,
details.section-accordion > summary > h5 {
    margin: 0;
    flex: 1;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    line-height: 1.3;
}
.section-accordion > summary > h3 svg,
.section-accordion > summary > h4 svg,
.section-accordion > summary > h5 svg,
.section-accordion > summary > h3 i,
.section-accordion > summary > h4 i,
.section-accordion > summary > h5 i { flex-shrink: 0; }
.section-accordion > summary .tab-count {
    font-weight: var(--fw-normal);
    color: var(--text-muted);
}
/* Content panel - card-like surface for the tab content area */
.detail-layout > div:last-child {
    background: var(--card);
    border-radius: var(--radius);
    border: 1px solid var(--border);
    padding: 1.25rem;
    box-shadow: var(--shadow-sm);
    min-width: 0;
    overflow: visible;
}
/* Flatten inner cards inside the detail content panel - avoid card-in-card */
.detail-layout > div:last-child > [data-tab-panel] > .card {
    border: none;
    box-shadow: none;
    padding: 0;
    background: transparent;
    border-radius: 0;
}

/* ── Status alerts ── */
.error-partial {
    padding: 0.75rem 1rem;
    background: var(--danger-light);
    border: 1px solid var(--danger-border-subtle);
    border-radius: var(--radius-sm);
    color: var(--danger);
    margin-bottom: 1rem;
}
.success-alert {
    padding: 0.75rem 1rem;
    background: var(--success-light);
    border: 1px solid var(--success-border-subtle);
    border-radius: var(--radius-sm);
    color: var(--success);
    margin-bottom: 1rem;
}

/* ── Page header ── */
.page-header {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    margin-bottom: 1rem;
    flex-wrap: wrap;
    gap: 0.6rem;
    text-align: left;
    /* view-transition-name removed - it created a stacking context that
       trapped actions-panel dropdowns below sibling content. The content-area
       transition already covers page-header swaps. */
}
/* white-space: nowrap so the title NEVER wraps mid-word. Combined
   with the .page-header { flex-wrap: wrap } above, the actions div
   wraps below the title at narrow widths instead of squeezing the
   title into a multi-line column. Makes <page-header> safely usable
   on pages with wide action slots (Dispatch had to revert to raw
   flex pre-fix because of this exact squeeze bug). */
.page-header h2, .page-header h3 { margin: 0; flex: 0 1 auto; min-width: 0; display: inline-flex; align-items: center; gap: 0.4rem; line-height: 1; white-space: nowrap; }
/* Page-header titles step down to --step-2 (~16-18px) from the prior
   --step-3 (18-22px). Combined with smaller icon + tighter button
   chrome below, the header collapses from ~60-70px tall to ~40-45px,
   recovering above-the-fold space without losing the title's
   visual prominence (it's still the largest text in the chrome). */
.page-header h2 { font-size: var(--step-2); }
.page-header h2 > svg, .page-header h3 > svg,
.page-header h2 > i, .page-header h3 > i { flex-shrink: 0; width: 1.125rem; height: 1.125rem; }
/* flex: 1 1 auto + min-width: max-content - the title-block grows to
   absorb extra space (auto basis) but never shrinks below its
   intrinsic content width. When the actions slot can't fit beside
   it, flex-wrap kicks in and the actions wrap to a new row instead
   of squeezing the title-block (which previously caused the day-nav
   on Dispatch to clip behind the action buttons). */
.page-header > div:first-child { min-width: max-content; flex: 1 1 auto; }
/* Detail-header exception to the max-content guard above. The guard protects a
   title-block from a SIBLING actions slot (Dispatch's day-nav). The detail-page
   header is a `.flex-col` that keeps its actions INSIDE the title row, so it has
   no sibling to protect - and its widest child is a long free-text title (KB /
   library article names) or the "Customer: ... | Equipment: ..." subtitle.
   max-content there pins the block to that single-line width and overflows
   whenever the content area is narrower than the content (sidebar included),
   at ANY width - so the fix is container-relative, not a viewport breakpoint.
   min-width: 0 lets the block fit its container; the title/row/subtitle then
   wrap within it via white-space/flex-wrap/overflow-wrap. Dispatch (a plain
   <div>, not .flex-col) keeps the guard. */
.page-header > .flex-col:first-child { min-width: 0; }
.page-header > .flex-col > .flex { flex-wrap: wrap; }
.page-header > .flex-col h2, .page-header > .flex-col h3 { white-space: normal; overflow-wrap: anywhere; }
/* Same exception for the <page-header title= subtitle=> tag-helper shape: it
   renders a plain title <div> (<h2> + <small> subtitle) with the actions as a
   sibling. The subtitle is the widest child, so max-content pins the block to
   the subtitle's single-line width and overflows narrow content areas (every
   admin page with a subtitle). Relax it wherever the title block carries a
   subtitle. Dispatch's title block holds a .day-nav, not a <small>, so it never
   matches and keeps the guard. */
.page-header > div:first-child:has(> small) { min-width: 0; }
/* Free-text detail titles (KB / library article names) scale a touch harder
   than the shared --step-2: ~15px on a phone so they take fewer wrapped lines,
   rising to the same 22px cap on desktop. Pure viewport-fluid, no breakpoint.
   Scoped to the detail-header so short entity-number titles on list/Dispatch
   headers keep --step-2. */
.page-header > .flex-col h2 { font-size: clamp(0.9375rem, 0.655rem + 0.9vw, 1.375rem); }
.page-header > div:not(:first-child):last-child,
.page-header > :last-child:not(h2):not(h3):not(:first-child) { flex-shrink: 0; display: flex; align-items: center; gap: 0.4rem; }
.page-header small { display: block; width: 100%; max-width: min(100%, 72ch); color: var(--text-muted); font-size: var(--text-sm); margin-top: 0.2rem; overflow-wrap: anywhere; }
/* Buttons in the header sit at ~1.75rem with text-sm so they read
   as compact action chips next to the title, not standalone
   buttons. Matches the .btn-sm chrome used elsewhere. */
.page-header a[role="button"],
.page-header a.outline,
.page-header .btn-delete { padding: 0.2rem 0.55rem; font-size: var(--text-sm); height: 1.75rem; display: inline-flex; align-items: center; }
/* Ensure forms inside action bars don't add extra spacing */
.actions form {
    margin: 0;
    padding: 0;
    display: inline-flex;
}
/* Action buttons in non-header contexts keep standard height */
.actions button,
.actions [role="button"] {
    height: 38px;
    display: inline-flex;
    align-items: center;
}

/* ── Actions Menu (three-dot dropdown) ──
   Replaces inline action button rows in page headers with a single
   trigger button that opens a floating panel.
   Usage:
     <div class="actions-menu" x-data="{ open: false }">
       <button type="button" class="actions-trigger" @click="open = !open" @click.outside="open = false">
         <i data-lucide="more-vertical"></i>
       </button>
       <div class="actions-panel" x-show="open" x-cloak x-transition>
         <a href="..." class="actions-item"><i data-lucide="pencil"></i> Edit</a>
         <a href="..." class="actions-item"><i data-lucide="copy"></i> Duplicate</a>
         <div class="actions-divider"></div>
         <a href="..." class="actions-item danger"><i data-lucide="trash-2"></i> Delete</a>
       </div>
     </div>
*/
.actions-menu {
    position: relative;
    display: inline-flex;
}
/* Hide the whole menu (trigger + panel) when no .actions-item is
   rendered. Happens when every item is role-gated and the current user
   has none - clicking the ⋮ would otherwise open an empty dropdown. */
.actions-menu:not(:has(.actions-item)) { display: none; }
/* Universal actions kebab style - same shape (round outline button,
   1.5px border, transparent fill) everywhere it appears (table-row
   leading cells, page-header overflow menus, info-card footers).
   Color tone is driven by the tone-* modifier class, which the
   <actions-menu tone="..."> tag helper emits. No tone class →
   primary (the default everywhere). Other tones let callers give
   the kebab semantic weight per context (danger when the only
   action is destructive, success when the dominant action is
   constructive, info/warning to mirror a card's status accent).
   Hover state inverts to a solid fill of the same tone for clear
   interactive feedback. */
.actions-trigger,
.actions-trigger[role="button"],
button.actions-trigger {
    /* CSS-variable-driven so each tone-* variant only redefines the
       --kebab-color token, not the whole rule body. The kebab is a
       "more options" affordance, not a CTA - defaulting to muted-gray
       lets row content (names, statuses, totals) stay the focus while
       still leaving the action handle reachable. Pass tone="primary"
       on the rare row where the kebab IS the prominent action. */
    --kebab-color: var(--text-muted);
    --kebab-on: var(--card);
    width: 32px;
    height: 32px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1.5px solid var(--kebab-color);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--kebab-color);
    cursor: pointer;
    transition: var(--transition-btn);
    box-shadow: none;
}
.actions-trigger:hover,
.actions-trigger[role="button"]:hover,
button.actions-trigger:hover,
.actions-trigger:focus-visible {
    border-color: var(--kebab-color);
    color: var(--kebab-on);
    background: var(--kebab-color);
}
/* Tone variants - only redefine the color token. Border + hover-fill
   pick it up automatically from the rule above. Default (no tone class)
   is muted-neutral; pass tone="primary" / "success" / "warning" / "danger"
   / "info" when the kebab should signal something specific. */
.actions-trigger.tone-primary { --kebab-color: var(--primary); --kebab-on: #fff; }
.actions-trigger.tone-success { --kebab-color: var(--success); --kebab-on: #fff; }
.actions-trigger.tone-warning { --kebab-color: var(--warning); --kebab-on: #fff; }
.actions-trigger.tone-danger  { --kebab-color: var(--danger);  --kebab-on: #fff; }
.actions-trigger.tone-info    { --kebab-color: var(--info);    --kebab-on: #fff; }
/* Kept as an alias of the new default (muted) so any caller still passing
   tone="neutral" continues to work without behavior change. */
.actions-trigger.tone-neutral { --kebab-color: var(--text-muted); --kebab-on: var(--card); }
.actions-panel {
    /* Native HTML popover. The browser handles open / close / Esc /
       click-outside via popover="auto" + popovertarget. Top-layer
       rendering escapes every ancestor overflow context - no clipping
       inside .card-table or anywhere else. JS in app.js sets top/left
       (or top/right) inline on the toggle event so the panel anchors
       to its trigger; otherwise the UA defaults to viewport-centered. */
    position: fixed;
    margin: 0;
    inset: auto;
    min-width: 200px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-xl);
    z-index: var(--z-popover);
    padding: 0.35rem 0;
    overflow: visible;
}
.actions-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0.9rem;
    font-size: var(--text-body);
    color: var(--text);
    text-decoration: none;
    cursor: pointer;
    transition: background var(--transition);
    /* Reset all button/anchor chrome - looks like a plain list item */
    border: none;
    background: transparent;
    width: 100%;
    font-family: inherit;
    font-weight: 400;
    white-space: nowrap;
    line-height: 1.4;
    -webkit-appearance: none;
    appearance: none;
    border-radius: 0;
    box-shadow: none;
}
.actions-item:hover {
    background: var(--bg);
    color: var(--heading);
    text-decoration: none;
}
.actions-item i[data-lucide], .actions-item svg.lucide {
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    color: var(--text-muted);
}
.actions-item:hover i[data-lucide], .actions-item:hover svg.lucide {
    color: var(--heading);
}
.actions-item.danger { color: var(--danger); }
.actions-item.danger i[data-lucide], .actions-item.danger svg.lucide { color: var(--danger); }
.actions-item.danger:hover { background: var(--danger-subtle-hover); }
.actions-item.success { color: var(--success); }
.actions-item.success i[data-lucide], .actions-item.success svg.lucide { color: var(--success); }
.actions-item.success:hover { background: var(--success-subtle-hover); }
.actions-divider {
    height: 1px;
    background: var(--border);
    margin: 0.25rem 0;
}
/* Forms inside the panel (for POST actions like delete, duplicate) */
.actions-panel form {
    margin: 0;
    padding: 0;
}
.actions-panel form .actions-item { width: 100%; }
/* Panel items look like plain list items, not buttons. The fill rules are
   :where()-wrapped (zero specificity), so these multi-class selectors win on
   their own - no !important needed for layout/border/background. Only `color`
   keeps !important: it must also beat the high-specificity dark-mode link
   rule for an <a class="actions-item"> (else the text renders primary-purple
   in dark mode). The rest-hover `background` keeps !important too - it shares
   specificity with the dark .actions-item:hover override below and must win. */
.actions-panel .actions-item,
.actions-panel button.actions-item,
.actions-panel a.actions-item,
.actions-panel [role="button"].actions-item {
    padding: 0.55rem 0.9rem;
    border: none;
    border-radius: 0;
    background: transparent;
    font-weight: 400;
    height: auto;
    box-shadow: none;
    justify-content: flex-start;
    gap: 0.6rem;
    color: var(--text) !important;
    font-size: var(--text-body);
}
.actions-panel .actions-item:hover {
    background: var(--bg) !important;
    color: var(--heading) !important;
}
.actions-panel .actions-item.danger {
    color: var(--danger) !important;
}
.actions-panel .actions-item.danger:hover {
    background: var(--danger-subtle-hover);
    color: var(--danger) !important;
}
.actions-panel .actions-item.success {
    color: var(--success) !important;
}
.actions-panel .actions-item.success:hover {
    background: var(--success-subtle-hover);
    color: var(--success) !important;
}

/* Round (radial) trigger variant - used by <row-actions-menu> for per-row
   action menus in tables. The base .actions-trigger is a square 32×32 with
   var(--radius-sm); this modifier locks it to a perfect circle. The width /
   height / min-width / min-height redeclarations are defensive: global
   [role="button"] / a:not(....) rules elsewhere can otherwise add padding /
   min-width that turns the trigger oblong on certain pages. */
.actions-trigger-round,
a.actions-trigger.actions-trigger-round,
button.actions-trigger.actions-trigger-round {
    width: 32px !important;
    height: 32px !important;
    min-width: 32px !important;
    min-height: 32px !important;
    max-width: 32px !important;
    max-height: 32px !important;
    border-radius: 50% !important;
    padding: 0 !important;
    flex: 0 0 auto;
    aspect-ratio: 1 / 1;
}
/* Anchor-as-trigger spec ("radial ahref button"). The base .actions-trigger
   already covers [role="button"]; an <a href="#"> needs the text-decoration
   reset to avoid an underline showing through the icon glyph. */
a.actions-trigger { text-decoration: none; }
a.actions-trigger:hover { text-decoration: none; }
/* The kebab icon inside should be small - 16px reads as a clean three-dot
   glyph at the round 32px button size. Lucide's default would otherwise
   inherit the page's larger icon scale. */
.actions-trigger i[data-lucide],
.actions-trigger svg.lucide {
    width: 16px;
    height: 16px;
}

/* Native popovers handle their own visibility: the UA hides the
   element when popover="auto" is closed, shows it when open. No
   .is-open class toggle, no :has() workaround for ancestor overflow -
   top-layer rendering escapes every overflow context for free. */

/* Dark mode - .actions-trigger no longer needs a theme override because
   the universal outline-primary style above uses --primary which is
   theme-aware (#696CFF light, #818CF8 dark) and works on both card
   surfaces and dark-bg surfaces equally. */
[data-theme="dark"] .actions-panel { border-color: var(--border); box-shadow: 0 8px 30px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3); }
[data-theme="dark"] .actions-item:hover { background: var(--input-bg); }

/* ── Timeline ── */
.timeline { border-left: 2px solid var(--border); padding-left: 1rem; margin: 1rem 0; }
.timeline-entry {
    margin-bottom: 1rem;
    position: relative;
    /* Skip layout + paint for entries far outside the viewport. A busy
       tenant's activity feed can have 500+ entries - this keeps scroll
       perf flat. contain-intrinsic-size reserves ~80px so the scrollbar
       doesn't jump as entries enter / leave the render window. */
    content-visibility: auto;
    contain-intrinsic-size: auto 80px;
}
.timeline-entry::before {
    content: '';
    position: absolute;
    left: -1.375rem;
    top: 0.35rem;
    width: 8px;
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--primary);
}
.timeline-entry small { color: var(--text); font-size: var(--text-sm); }
.timeline-entry p { margin: 0.15rem 0 0; font-size: var(--text-body); }

/* ── Notification list row ──
   Stateful row concerns that can't be done with utilities: border-bottom
   between siblings, hover, unread accent (left border + tint). Wrap it in any
   bordered container (e.g. .card with .p-0). Everything inside the row uses
   utility classes. */
.notification-row {
    border-bottom: 1px solid var(--border);
    border-left: 3px solid transparent;
    padding: 0.75rem 1rem;
    transition: background var(--transition-fast);
    /* Same lazy-render treatment as .timeline-entry - the notifications
       page can list hundreds of rows. */
    content-visibility: auto;
    contain-intrinsic-size: auto 80px;
}
.notification-row:last-child { border-bottom: 0; }
.notification-row:hover { background: var(--bg); }
.notification-row.is-unread { border-left-color: var(--primary); background: rgba(var(--primary-rgb), 0.04); }
.notification-row.is-unread:hover { background: rgba(var(--primary-rgb), 0.08); }
.notification-row.is-unread .notification-title { color: var(--primary); }
/* Inline action links (View / Mark read / Delete) - muted by default, */
/* primary on hover, with a .danger-link variant for destructive actions. */
.link-muted { color: var(--text-muted); text-decoration: none; }
.link-muted:hover { color: var(--primary); text-decoration: underline; }
.link-muted.danger-link:hover { color: var(--danger); }

/* ── Pagination ── */
nav[aria-label="Pagination"] ul {
    display: flex;
    gap: 0.25rem;
    list-style: none;
    align-items: center;
    justify-content: center;
    padding: 1rem 0 0;
}
nav[aria-label="Pagination"] a {
    padding: 0.35rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    border: 1px solid var(--border);
    color: var(--text);
}
nav[aria-label="Pagination"] a:hover {
    background: var(--primary);
    color: var(--white);
    border-color: var(--primary);
    text-decoration: none;
}
nav[aria-label="Pagination"] a[aria-disabled="true"] {
    opacity: 0.4;
    pointer-events: none;
}
nav[aria-label="Pagination"] span {
    font-size: var(--text-sm);
    padding: 0 0.5rem;
    color: var(--text);
}

/* ── Lucide icons in buttons/nav ── */
/* Scale Lucide icons relative to the parent's font-size so they stay
   proportional across button sizes (regular, btn-sm, headings, etc.)
   without pushing the element taller than its text content.
   1.15em ≈ 16px in a 0.875rem button, ~13px in btn-sm, ~20px in h2. */
button i[data-lucide], button svg.lucide,
[role="button"] i[data-lucide], [role="button"] svg.lucide,
.btn i[data-lucide], .btn svg.lucide,
.outline i[data-lucide], .outline svg.lucide,
.btn-delete i[data-lucide], .btn-delete svg.lucide,
.ghost i[data-lucide], .ghost svg.lucide,
.secondary i[data-lucide], .secondary svg.lucide {
    width: 1.15em;
    height: 1.15em;
    flex-shrink: 0;
}
.sidebar-nav a i[data-lucide], .sidebar-nav a svg {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

/* ══════════════════════════════════════════
   APP LAYOUT
   ══════════════════════════════════════════ */

.app-layout { display: flex; min-height: 100dvh; }

/* ── Sidebar ── */
.sidebar {
    width: var(--sidebar-width);
    background: var(--sidebar);
    position: fixed;
    top: 0; left: 0; bottom: 0;
    overflow-y: auto;
    z-index: var(--z-sidebar);
    display: flex;
    flex-direction: column;
    transition: transform var(--transition-slow) ease;
}

/* Scrollbar */

.sidebar-brand {
    padding: 1.25rem 1.5rem;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    border-bottom: 1px solid var(--border-light);
}
.sidebar-brand .brand-icon {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    background: var(--primary);
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--white);
    font-weight: 700;
    font-size: var(--step-2);
    cursor: pointer;
    transition: transform var(--transition-base), box-shadow var(--transition-base);
}
.sidebar-brand .brand-icon .brand-logo {
    width: 20px;
    height: auto;
    filter: brightness(0) invert(1); /* sidebar always inverted on purple bg */
}
.sidebar-brand .brand-icon:hover {
    transform: scale(1.1);
    box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.3);
}
.sidebar-brand span {
    font-family: var(--font-display);
    color: var(--sidebar-brand-text);
    font-size: var(--step-1);
    font-weight: 700;
    letter-spacing: -0.02em;
    /* Long business names shrink to fit + ellipsize instead of overflowing
       the sidebar (min-width:0 lets this flex child shrink below content). */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sidebar-nav { flex: 1; padding: 0.5rem 0; }

.sidebar-nav .nav-section {
    font-family: var(--font-display);
    padding: 0.75rem 1.25rem 0.3rem;
    /* Transparent left border matches the 3px active-stripe on nav items
       below, keeping section labels visually aligned with nav-item text. */
    border-left: 3px solid transparent;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--sidebar-section);
}

/* Collapsible nav section toggles - native <details>/<summary>, so the
   browser owns toggle, Enter/Space, focus, and disclosure semantics.
   The (0,2,0) selectors here out-specify the global "summary as pill
   badge" theme (0,1,1) for the shared properties; background and the
   +/- ::after marker are the only pill declarations that need explicit
   neutralizing (same scoped-override approach as .cat-tree summaries -
   no :not() exclusion entries). */
.sidebar-nav .nav-section-toggle {
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
    user-select: none;
    background: none;
    border-radius: 0;
}
.sidebar-nav .nav-section-toggle::after { content: none; }
.sidebar-nav .nav-section-toggle:hover {
    color: var(--primary);
    background: none;
}
/* The pill theme's open-state rule (details[open] > summary:not(...)) lands
   at (0,2,2), so the open state needs its own (0,3,1) re-statement. */
.sidebar-nav details[open] > .nav-section-toggle {
    background: none;
    color: var(--sidebar-section);
}
.sidebar-nav details[open] > .nav-section-toggle:hover { color: var(--primary); }
.sidebar-nav details[open] > .nav-section-toggle::after { content: none; }
/* Keyboard focus ring (summary is natively focusable) */
.sidebar-nav .nav-section-toggle:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}
.sidebar-nav .nav-chevron {
    width: 14px;
    height: 14px;
    transition: transform var(--transition-base) ease;
    flex-shrink: 0;
}
.sidebar-nav details:not([open]) .nav-chevron { transform: rotate(-90deg); }

/* Sidebar collapse icon toggle - pure CSS, no JS needed */
.icon-when-collapsed { display: none !important; }
.sidebar-collapsed .icon-when-expanded { display: none !important; }
.sidebar-collapsed .icon-when-collapsed { display: inline-block !important; }

/* Full-width nav rows (not inset pills) with a left-accent stripe for the
   active item - matches the standard sidebar pattern seen in most admin UIs.
   Padding trimmed slightly from 0.7→0.55rem + 1.5→1.25rem to tighten density
   without crowding icons or touch targets. */
.sidebar-nav a {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 1.25rem;
    margin: 0;
    min-height: 38px;
    text-decoration: none;
    color: var(--sidebar-text);
    font-size: var(--text-sm);
    /* Sidebar nav rides lighter than --fw-body (380) so the menu reads as a
       quiet wayfinding column, not a wall of bold. 320 sits at the lighter
       end of Public Sans's wght axis; still legible against the dark
       sidebar, but optically a tier under page-body weight. Variable font
       requires both font-weight and the 'wght' axis to track. */
    font-weight: 320;
    font-variation-settings: 'wght' 320;
    border-left: 3px solid transparent;
    transition: var(--transition-btn);
}
/* Pair the lighter text with thinner icon strokes; Lucide ships at
   stroke-width: 2 by default, which reads chunky next to wght 320 text.
   1.5 keeps glyphs crisp without dominating the row. */
.sidebar-nav a svg { stroke-width: 1.5; }
.sidebar-nav a:hover {
    background: var(--sidebar-hover);
    color: var(--sidebar-brand-text);
}
.sidebar-nav a.active {
    background: var(--sidebar-active-bg);
    color: var(--primary);
    /* No font-weight bump; active state is already telegraphed by the
       primary-tint background, primary text color, and the 3px primary
       left stripe. Keeping the active item at the inherited --fw-body
       (380) means the row sits at the same optical weight as the other
       nav items, so the highlight reads as a "current section" tint,
       not a "louder" entry. The icon stroke (lucide default 2px) also
       stays inherited, matching the rest of the menu. */
    border-left-color: var(--primary);
}
.sidebar-nav a.active svg { color: var(--primary); }

/* ── Sidebar collapse toggle ── */
.sidebar-toggle {
    padding: 0.75rem 1rem;
    border-top: 1px solid rgba(255,255,255,0.06);
}
.sidebar-collapse-btn {
    width: 100%;
    justify-content: center;
    color: var(--sidebar-text) !important;
    padding: 0.4rem !important;
    border-radius: var(--radius-sm);
}
.sidebar-collapse-btn:hover { background: var(--sidebar-hover) !important; color: var(--sidebar-brand-text) !important; }
.sidebar-collapse-btn svg { width: 18px; height: 18px; }

/* ── Collapsed sidebar state ── */
.sidebar-collapsed .sidebar {
    width: 68px;
    /* Shorthand `overflow: hidden` was killing both axes; vertical
       scroll vanished, leaving ~36 of 48 nav items unreachable in mobile
       drawer + desktop short-viewport modes. Two-value form clips
       horizontally (the original intent - hide nav-labels overflowing the
       68px column) while keeping vertical scroll. */
    overflow: hidden auto;
}
.sidebar-collapsed .sidebar-brand .nav-label { display: none !important; }
.sidebar-collapsed .sidebar-brand { justify-content: center; padding: 1.25rem 0.5rem; overflow: hidden; }
/* brand-icon stays visible when collapsed - it IS the toggle */
.sidebar-collapsed .sidebar-nav .nav-section { text-align: center; padding: 0.75rem 0 0.2rem; }
.sidebar-collapsed .sidebar-nav .nav-section .nav-label { display: none; }
.sidebar-collapsed .sidebar-nav .nav-section::after {
    content: '';
    display: block;
    width: 20px;
    border-top: 1px solid var(--sidebar-hover);
    margin: 0 auto;
}
/* Collapsed mini-rail: icon-only centered buttons. Drops the left stripe
   (no room to show it in a 42px-wide column without off-centering the icon)
   and restores a pill radius so the hover/active background doesn't render
   as a hard square. */
.sidebar-collapsed .sidebar-nav a {
    justify-content: center;
    padding: 0.7rem;
    margin: 0.125rem 0.5rem;
    min-height: 40px;
    border-left: none;
    border-radius: var(--radius-sm);
}
.sidebar-collapsed .sidebar-nav a.active {
    border-left-color: transparent;
}
.sidebar-collapsed .sidebar-nav a .nav-label { display: none; }
.sidebar-collapsed .sidebar-nav a .unread-badge { margin-left: 0 !important; position: absolute; top: 2px; right: 2px; }
.sidebar-collapsed .sidebar-nav a { position: relative; }
.sidebar-collapsed .sidebar-toggle { padding: 0.5rem 0.6rem; }
.sidebar-collapsed .main-content { margin-left: 68px; }

/* Tooltip on hover when collapsed */
.sidebar-collapsed .sidebar-nav a[title] { position: relative; }
.sidebar-collapsed .sidebar-nav a[title]:hover::after {
    content: attr(title);
    position: absolute;
    left: calc(100% + 12px);
    top: 50%;
    transform: translateY(-50%);
    background: var(--sidebar);
    color: var(--sidebar-brand-text);
    padding: 0.3rem 0.65rem;
    border-radius: var(--radius-xs);
    font-size: var(--text-sm);
    white-space: nowrap;
    z-index: var(--z-tooltip);
    box-shadow: 0 2px 8px rgba(0,0,0,0.25);
    pointer-events: none;
}
.sidebar-collapsed .sidebar-nav a[title]:hover::before {
    content: '';
    position: absolute;
    left: calc(100% + 6px);
    top: 50%;
    transform: translateY(-50%);
    border: 5px solid transparent;
    border-right-color: var(--sidebar);
    z-index: var(--z-tooltip);
}

/* Transition for smooth collapse */
.sidebar { transition: width var(--transition-base) ease; }
.nav-label { transition: opacity var(--transition-base) ease; white-space: nowrap; }

/* ── SystemAdmin shell ──
   Adds the slim admin-area chrome around the standard .app-layout +
   .sidebar + .main-content stack. Most styles are inherited from those
   classes; only the breadcrumb + identity strip + cross-page density
   bumps are scoped here. The .admin-area class is applied at .app-layout
   level so descendants can opt in via plain selector scoping. */
.admin-area .admin-topbar { padding: 0.5rem 1.25rem; min-height: 48px; }
.admin-area .admin-breadcrumb { display: flex; align-items: center; gap: 0.5rem; }
.admin-area .admin-breadcrumb .icon-xs { color: var(--text-muted); }
.admin-area .admin-identity { display: flex; align-items: center; gap: 0.75rem; padding-left: 0.75rem; border-left: 1px solid var(--border-light); }
.admin-area .admin-identity-name { font-size: var(--text-sm); color: var(--text-muted); }
/* Mobile nav toggle: hidden on desktop (sidebar always visible), revealed once
   the sidebar goes off-canvas at <=768px (see that media block). */
.admin-menu-btn { display: none; }

/* Density: tighter table rows and reduced section gaps app-wide for the
   admin area. Operators scan dense data; the regular app's whitespace-
   first spacing makes SystemAdmin feel oversized. */
.admin-area .card { padding: 1rem; }
.admin-area .card.mb-lg, .admin-area .card.mb-xl { margin-bottom: 1rem; }
.admin-area table th, .admin-area table td { padding: 0.45rem 0.6rem; }
.admin-area table th { font-size: var(--text-xs); letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.admin-area .page-header { margin-bottom: 0.75rem; }
.admin-area .form-grid { gap: 0.6rem 0.75rem; }
/* Responsive: dense admin tables that don't stack on mobile (observability
   .data-table, platform-config .config-table) scroll horizontally within their
   container instead of overflowing the page - same idea as the main app's
   .card-table wrapper. Cards holding a .table-stacked-mobile are left alone so
   they keep collapsing into cards. */
.admin-area .card:has(table:not(.table-stacked-mobile)),
.admin-area .config-section:has(.config-table) { overflow-x: auto; }

/* Scaled-down badges inside any list-row table (the .card-table
   wrapper). The default .badge sizing (text-sm + 0.2/0.7rem padding)
   was originally calibrated for tenant-app rows where a status badge
   was often the only visual weight in a wide cell - but modern list
   rows carry an actions-menu kebab, an entity-link, the badge, and
   several at-a-glance columns. The badge isn't the only visual weight
   anymore, so it should read as a label, not a button. .admin-area
   keeps its own rule so badges outside card-tables in admin contexts
   (Dashboard, PlatformConfig) stay compact too. Both .badge AND
   .kb-type-badge (the icon pill on Library / KB list rows) collapse
   to the same scale so a row carrying both reads as one visual weight. */
.card-table .badge,
.admin-area .badge {
    font-size: var(--text-xs);
    padding: 0.1rem 0.5rem;
    border-radius: var(--radius-sm);
    line-height: 1.4;
}
.card-table .kb-type-badge,
.admin-area .kb-type-badge {
    font-size: var(--text-xs);
    padding: 0.1rem 0.5rem;
    gap: 0.25rem;
    border-radius: var(--radius-sm);
    line-height: 1.4;
}
.card-table .kb-type-badge i,
.admin-area .kb-type-badge i { width: 10px; height: 10px; }

/* ── Admin page header overrides ──
   The shared <page-header> tag helper renders the same shape app-wide;
   .admin-area scope just trims it down: smaller h2, one-row layout,
   bottom border, less margin. No parallel admin-specific tag helper. */
.admin-area .page-header {
    align-items: center;
    gap: 1rem;
    padding: 0.4rem 0 0.75rem;
    margin-bottom: 1rem;
    border-bottom: 1px solid var(--border);
    flex-wrap: wrap;
}
.admin-area .page-header h2 {
    margin: 0;
    font-size: var(--text-h4);
    font-weight: 600;
    line-height: 1.2;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--heading);
}
.admin-area .page-header h2 i { color: var(--primary); flex-shrink: 0; }
.admin-area .page-header small { color: var(--text-muted); font-size: var(--text-xs); display: block; }
/* The tag helper's actions slot is a sibling <div> on the right. */
.admin-area .page-header > div:not(.flex-col):not(.stat-chips) {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
    flex-wrap: wrap;
}

/* Inline KPI strip - chips of label/value pairs that live INSIDE the
   admin-page-header (or anywhere on an admin page where a stat row beats
   a tile grid). Same data as <kpi-card> but ~30px tall instead of ~150. */
.stat-chips {
    display: flex;
    /* justify-content: space-between spreads the chips fluidly across
       the available width - each chip stays at its natural content
       width, the GAPS grow to fill the row. The gap value below is a
       minimum: with flex-wrap on, it's what kicks in once the chips
       start wrapping or when there's no extra horizontal space left. */
    justify-content: space-between;
    gap: 1.25rem;
    align-items: center;
    flex-wrap: wrap;
    padding: 0 0.75rem;
    border-left: 1px solid var(--border);
    border-right: 1px solid var(--border);
    margin-right: 0.5rem;
}
/* Mobile: fill the container and let the chips wrap, instead of sizing to
   max-content and overflowing. The SystemAdmin observability headers
   (subscriptions / tenant-health / webhooks) pack enough chips to run ~500px
   wide; without this they pushed the whole page sideways on a phone. */
@media (width <= 768px) {
    .stat-chips {
        width: 100%;
        min-width: 0;
        flex-shrink: 1;
        justify-content: flex-start;
        gap: 0.6rem 1.25rem;
    }
}
.stat-chip { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.stat-chip-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    font-weight: 500;
}
.stat-chip-value {
    font-size: var(--step-2);
    font-weight: 600;
    color: var(--heading);
    font-family: var(--font-display);
    line-height: 1.1;
}
.stat-chip-value.danger { color: var(--danger); }
.stat-chip-value.success { color: var(--success); }
.stat-chip-value.warning { color: var(--warning); }
.stat-chip-value.info { color: var(--info); }
.stat-chip-value.primary { color: var(--primary); }
/* Smaller value variant - for chips carrying a date range / label rather
   than a headline number (e.g. the Payroll "Period" chip). */
.stat-chip-value--sm { font-size: var(--text-sm); }
/* Optional third row under the value - human-readable bucket descriptor
   (e.g. "Mixed", "Good", "% who said easy"). Sits inside the same
   flex-column .stat-chip so the chip stays compact. */
.stat-chip-sub {
    font-size: var(--text-xs);
    color: var(--text-muted);
    line-height: 1.2;
    margin-top: 0.1rem;
}

/* ── Customer-feedback mention clouds (/surveys home) ──
   Tag-cloud-style sentiment surfaces - "What customers love" /
   "Where customers struggle". Each chip is a keyword + frequency,
   sized + tinted by its share of the maximum count for that side.
   The --w custom property (0..1) is stamped per-chip from C#:
     style="--w:@(count/(double)max)"
   and feeds calc()s for font-size, padding, and background opacity.
   Result: dominant themes read loudest, low-frequency mentions
   recede - the cloud shape makes "we keep hearing about X" obvious
   at a glance without a stacked-bar chart. */
.mention-card { padding: 1.25rem; container-type: inline-size; }
.mention-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.9rem;
}
.mention-card-head h3 {
    font-size: var(--text-body);
    font-weight: var(--fw-semibold);
    color: var(--heading);
    display: flex;
    align-items: center;
}
.mention-card-total {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-weight: var(--fw-medium);
    letter-spacing: 0.01em;
}

.mention-cloud {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.55rem;
}

/* Scoped to .mention-cloud: the bare .mention-chip class is ALSO the
   @mention chip in note bodies (styled near the Notes block) - unscoped,
   these survey-cloud sizing rules silently won the cascade and inflated
   every note mention. */
.mention-cloud .mention-chip {
    --w: 0.5;
    display: inline-flex;
    align-items: baseline;
    gap: 0.35rem;
    padding: calc(0.25rem + 0.15rem * var(--w)) calc(0.55rem + 0.35rem * var(--w));
    border-radius: var(--radius-pill);
    font-weight: var(--fw-semibold);
    font-size: calc(0.8125rem + 0.45rem * var(--w));
    line-height: 1.2;
    letter-spacing: -0.005em;
    transition: transform 0.12s ease, filter 0.12s ease;
}
.mention-cloud .mention-chip:hover { transform: translateY(-1px); filter: brightness(1.04); }
.mention-chip-count {
    font-size: 0.7em;
    font-weight: var(--fw-medium);
    opacity: 0.75;
    letter-spacing: 0.01em;
}
/* Tone variants use color-mix to fade tint with --w. The most-mentioned
   chip sits at full tint; quieter ones at 30% so the eye picks up the
   loudest first. */
.mention-chip--love {
    background: color-mix(in srgb, var(--success) calc(15% + 25% * var(--w)), transparent);
    color: var(--success);
}
.mention-chip--struggle {
    background: color-mix(in srgb, var(--danger) calc(15% + 25% * var(--w)), transparent);
    color: var(--danger);
}

[data-theme="dark"] .mention-chip--love {
    background: color-mix(in srgb, var(--success) calc(20% + 30% * var(--w)), transparent);
    color: color-mix(in srgb, var(--success) 80%, white);
}
[data-theme="dark"] .mention-chip--struggle {
    background: color-mix(in srgb, var(--danger) calc(20% + 30% * var(--w)), transparent);
    color: color-mix(in srgb, var(--danger) 80%, white);
}

/* Empty state - bigger / friendlier than a one-line muted text fallback. */
.mention-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.6rem;
    padding: 1.25rem 0.5rem 0.5rem;
}
.mention-empty p { max-width: 22rem; line-height: 1.5; }

/* ── Survey response answers (/surveys/responses/{id}) ──
   Pre-fix each Q+A pair was a single .py-sm row with a bottom border;
   readable but cramped (label + value squeezed into the same line-height
   rhythm as a table row, every prompt looked like an item in a list).
   Slight vertical breathing room + a clearer head→body hierarchy. */
.response-answers {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}
.response-answer {
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--border);
}
.response-answer:last-child { border-bottom: none; }
.response-answer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.45rem;
}
.response-answer-prompt {
    font-size: var(--text-sm);
    font-weight: var(--fw-semibold);
    color: var(--heading);
    flex: 1;
    min-width: 0;
}
.response-answer-body {
    font-size: var(--text-sm);
    color: var(--text);
    line-height: 1.5;
}
.response-answer-stars {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
}
.response-answer-text {
    margin: 0;
    white-space: pre-wrap;
    font-size: var(--text-sm);
    color: var(--text);
}

/* Compact filter pill row - replaces the full filter card with a slim
   row of inline-filter chips above a table. Use form action+method on
   the .admin-filters element and inputs/selects sit alongside the apply
   button on the same line. */
.admin-filters {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}
.admin-filters label { display: flex; flex-direction: column; gap: 0.1rem; font-size: var(--text-xs); color: var(--text-muted); margin: 0; }
/* Inline label variant: caption sits left of the control on one row, so the
   control baseline-aligns with the bare search box instead of dropping below
   a stacked caption (Leads pipeline "Window" select). */
.admin-filters label.filter-inline { flex-direction: row; align-items: center; gap: 0.4rem; }
.admin-filters input, .admin-filters select {
    padding: 0.3rem 0.55rem;
    font-size: var(--text-sm);
    min-height: 30px;
    margin: 0;
}
.admin-filters button {
    padding: 0.35rem 0.7rem;
    font-size: var(--text-sm);
    margin-top: 0.9rem;
}
/* Icon-only actions (e.g. the Save-view bookmark) sit inline with the row at
   their natural 32x32 size - opt out of the label-offset margin + text-button
   padding the bare-button rule above applies for label-stacked filter bars. */
.admin-filters .btn-icon { margin-top: 0; padding: 0; }
.admin-filters .admin-filter-clear {
    margin-top: 0.9rem;
    font-size: var(--text-xs);
    text-decoration: none;
    color: var(--text-muted);
    padding: 0.35rem 0.5rem;
}
/* Vertical hairline separating two .admin-filters groups on the same
   row (e.g. Payroll Export's "Calculate" form + "Mark exported" form).
   Lets distinct workflows render flush together without reading as one
   form. Hidden on narrow viewports where the groups wrap to stacks. */
.admin-filters .admin-filters-divider {
    display: inline-block;
    width: 1px;
    align-self: stretch;
    background: var(--border);
    margin: 0 0.25rem;
}
@media (width <= 640px) {
    .admin-filters .admin-filters-divider { display: none; }
}

/* Quick pay-period preset chips (Payroll Export). A label + a wrapping
   row of small ghost buttons that set the date range in one click. */
.payroll-presets {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}
.payroll-presets .label-caps { margin-right: 0.15rem; }

/* ── Platform Config viewer (/system/config) ──
   Read-only key/value catalog of every config the app reads. Grouped
   by section, each row carries: key + description, kind chip, current
   value (masked for secrets), default, status pill. */
.config-section { margin-bottom: 1.5rem; }
.config-section-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--heading);
    margin: 1.25rem 0 0.4rem;
    padding-bottom: 0.35rem;
    border-bottom: 1px solid var(--border);
}
.config-section-title i { width: 16px; height: 16px; color: var(--primary); }
.config-section-note { color: var(--text-muted); font-size: var(--text-xs); margin: 0 0 0.5rem; }

.config-table { width: 100%; border-collapse: collapse; }
.config-table th {
    text-align: left;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    padding: 0.35rem 0.6rem;
    border-bottom: 1px solid var(--border);
    font-weight: 600;
}
.config-table td {
    padding: 0.5rem 0.6rem;
    vertical-align: top;
    border-bottom: 1px solid var(--border-light, var(--border));
}
.config-col-kind { width: 90px; }
.config-col-status { width: 110px; }
.config-col-chev { width: 28px; }
.config-table tr.config-row-override { background: color-mix(in srgb, var(--primary) 4%, transparent); }

.config-key {
    font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace);
    font-size: var(--text-sm);
    color: var(--heading);
    background: var(--bg);
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-xs);
    display: inline-block;
}

.config-value {
    font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace);
    font-size: var(--text-sm);
    color: var(--text);
    word-break: break-all;
}
.config-value-default { color: var(--text-muted); font-style: italic; }
.config-value-secret { color: var(--info); letter-spacing: 0.05em; }

.config-kind {
    display: inline-block;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.15rem 0.45rem;
    border-radius: var(--radius-xs);
    font-weight: 600;
}
.config-kind-secret { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info); }
.config-kind-path { background: color-mix(in srgb, var(--text-muted) 15%, transparent); color: var(--text-muted); }
.config-kind-tunable { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); }
.config-kind-value { background: color-mix(in srgb, var(--primary) 12%, transparent); color: var(--primary); }

/* ── SystemAdmin Dashboard ──
   Compact above-the-fold layout: stacked stat strips, then activity
   table at full width. The dashboard-stats-strip is a container so its
   children can size themselves by the strip's own width (not viewport)
   via @container - lets the chip gap shrink smoothly as the strip
   narrows under a wider sidebar or split-screen. */
.dashboard-stats-strip {
    display: flex;
    flex-direction: column;
    gap: clamp(0.35rem, 0.5cqi, 0.75rem);
    container-type: inline-size;
    container-name: dashboard-strip;
}
.dashboard-stats-strip .stat-chips {
    border: none;        /* the strip is its own row - no inner dividers */
    padding: 0;
    margin: 0;
    /* Grid + auto-fit + minmax distributes the chips evenly across the
       strip's full width instead of leaving them tight-left. Each chip
       occupies an equal 1fr column with a fluid floor that shrinks +
       grows with the container (via cqi). Wraps to a new row only when
       chips would shrink below the floor. */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, clamp(6rem, 9cqi, 10rem)), 1fr));
    gap: clamp(0.5rem, 1.25cqi, 1.25rem) clamp(0.75rem, 2cqi, 1.5rem);
    align-items: baseline;
}
.dashboard-section-title {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    font-weight: 600;
    margin: 0;
}

/* ── Main content area ── */
.main-content {
    flex: 1;
    margin-left: var(--sidebar-width);
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    transition: margin-left var(--transition-base) ease;
    /* Flex items default to min-width:auto, which makes them refuse to shrink
       below their content's intrinsic width. Any page with a wide inner flex
       layout (Leads pipeline, dispatch board, wide tables) would push this
       element past the viewport and force horizontal page scroll. Setting
       min-width:0 lets children's overflow-x:auto properly contain their
       own scrolling instead of leaking to the whole page. */
    min-width: 0;
}

/* ── Top bar ── */
.top-bar {
    position: sticky;
    top: 0;
    z-index: var(--z-topbar);
    background: var(--card);
    box-shadow: var(--shadow-sm);
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    padding: 0 1.5rem;
    justify-content: space-between;
}
.top-bar-left {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-shrink: 0;
    min-width: 0;
}
/* Default app logo in topbar when no tenant logo is uploaded */
.topbar-brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    color: inherit;
}
.topbar-brand:hover { text-decoration: none; }
.topbar-brand-badge {
    height: 36px;
    padding: 0 1.25rem;
    background: var(--primary);
    border-radius: var(--radius-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.topbar-brand-badge img {
    height: 18px;
    width: auto;
}
.top-bar-right {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 1;
    min-width: 0;
    flex-wrap: nowrap;
}
/* Avatar circle used in the topbar account dropdown. `flex-shrink: 0` prevents
   the circle from being squished into an ellipse when the topbar flex row gets
   tight on narrow viewports - the previous bug the user noticed. */
.user-avatar,
.user-avatar-btn {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    border-radius: var(--radius-full);
    background: var(--primary-light);
    color: var(--primary);
    font-weight: 600;
    font-size: var(--text-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    padding: 0;
}
/* The avatar-as-button variant used in the topbar - no native button chrome,
   just the circle. Matches .topbar-icon-btn hover feedback for consistency. */
.user-avatar-btn {
    border: none;
    cursor: pointer;
    transition: transform var(--transition-base) ease, box-shadow var(--transition-base) ease;
}
.user-avatar-btn:hover { transform: scale(1.05); box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.user-avatar-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }



/* ── Page body ── */
.main-body {
    padding: 1.5rem;
    flex: 1;
    /* Same rationale as .main-content - without min-width:0, inner wide layouts
       (pipeline, wide tables) push the viewport horizontal scrollbar instead of
       scrolling inside their own overflow-x:auto container. */
    min-width: 0;
    /* Establish an inline-size container so descendants can size themselves
       relative to the actual content area width (page area minus sidebar
       minus padding) via cqi. .info-card-grid uses this for fluid track
       sizing - a card grid in a 700px detail panel and the same grid on a
       wide list both pack the right number of columns without caring what
       the window width is. */
    container-type: inline-size;
    container-name: pagebody;
}

.app-footer {
    margin-top: 3rem;
    padding: 1.25rem 1.5rem;
    border-top: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--text-sm);
    text-transform: lowercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.app-footer-top {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.app-footer-name {
    font-family: var(--font-display);
    font-weight: 600;
}
.app-footer-tagline {
    font-family: var(--font-body);
    font-style: italic;
}
.app-footer-help {
    font-family: var(--font-body);
}
.app-footer-logo {
    height: auto;
    padding: 0.4rem 1rem;
    background: var(--primary);
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
}
.app-footer-logo img {
    height: 16px;
    width: auto;
    /* filter handled by .logo-inverted */
}
.app-footer a {
    color: var(--text-muted);
    text-decoration: none;
    transition: color var(--transition-base);
}
.app-footer a:hover { color: var(--primary); }

/* Unified KPI card - used on Dashboard AND Reports.
   Layout: label top, big number below, small icon top-right corner.
   Value sizing + compact-variant logic is driven by container width (the card
   itself), not viewport - so a 180px card in a 6-col dashboard row and a
   400px card in a 2-col report grid each look right regardless of viewport. */
.kpi-card {
    container-type: inline-size;
    container-name: kpi;
    background: var(--card);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    /* Padding compacted by ~25% (1rem → 0.7rem vert, 1.125rem → 0.9rem horiz)
       so dashboards stop bleeding below the fold. With 6+ tiles per row and
       2-3 rows of customizable tiles + Reports' 60+ tiles across sections,
       the prior padding burned ~150-200px of chrome per page. The value
       clamp is unchanged so the headline number still scales with card
       width via container queries. */
    padding: 0.7rem 0.9rem;
    cursor: help;
    transition: transform var(--transition-base) ease, box-shadow var(--transition-base) ease;
    position: relative;
    border: 1px solid transparent;
}
.kpi-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover);
    border-color: rgba(105,108,255,0.1);
}
/* Clickable variant - applied when the card has a drill-down URL.
   Renders as <a href="#"> so it dodges the base button rule that
   slams [role="button"] / <button> with var(--primary) background.
   These overrides null out the anchor defaults (browser-blue color,
   underline) and switch the cursor from "help" to "pointer". */
.kpi-card--clickable,
.kpi-card--clickable:hover,
.kpi-card--clickable:focus,
.kpi-card--clickable:active {
    color: inherit;
    text-decoration: none;
    cursor: pointer;
}
/* Resting affordance - a faint neutral border tells the user the
   card is interactive without being loud. Pre-Q5.1 the card had
   `border: 1px solid transparent` and only painted a primary-tinted
   border on hover; that worked for "I'm hovering" but missed "I'm
   clickable" at rest. The .kpi-card:hover rule (more specific) still
   wins on hover, so the primary-tinted hover state is preserved. */
.kpi-card--clickable {
    border-color: var(--border);
}
.kpi-icon {
    position: absolute; top: 0.75rem; right: 0.75rem;
    width: 28px; height: 28px; border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    opacity: 0.7;
}
.kpi-icon svg { width: 14px; height: 14px; }
.kpi-icon.primary { background: var(--primary-light); color: var(--primary); }
.kpi-icon.success { background: var(--success-light); color: var(--success); }
.kpi-icon.warning { background: var(--warning-light); color: var(--warning); }
.kpi-icon.info { background: var(--info-light); color: var(--info); }
.kpi-icon.danger { background: var(--danger-light); color: var(--danger); }

.kpi-content { min-width: 0; padding-right: 2rem; /* space for icon */ }
.kpi-label { font-size: var(--text-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; margin-bottom: 0.3rem; line-height: 1.3; }
.kpi-value { font-family: var(--font-display); font-size: clamp(1.25rem, 1rem + 4cqw, 1.75rem); font-weight: 700; color: var(--heading); line-height: 1.15; letter-spacing: -0.02em; word-break: break-word; }
.kpi-sub { font-size: var(--text-xs); color: var(--text-muted); margin-top: 0.2rem; opacity: 0.8; }

/* Compact variant - KPI card under 200px (narrow lanes, sidebar mini-panels).
   Triggers on container (card) width, not viewport, so it works identically
   wherever the card gets HTMX-swapped to. */
@container kpi (max-width: 200px) {
    .kpi-card { padding: 0.75rem 0.875rem; }
    .kpi-icon { top: 0.5rem; right: 0.5rem; width: 24px; height: 24px; }
    .kpi-content { padding-right: 1.5rem; }
    .kpi-label { font-size: var(--text-xs); }
}

/* KPI card tooltips render through the shared top-layer #ui-tooltip (data-tip
   on the card, driven by app.js) - see the .ui-tooltip block. */

/* ── Modal ── */
/* Base dialog rule - the only remaining consumers are the branded
   confirm-dialog (#confirm-dialog, for hx-confirm) and the fullscreen
   image preview (#image-preview-dialog). Form modals have migrated to the
   Popover API (.popover-panel + #popover in _Layout.cshtml). */
dialog {
    border: none;
    border-radius: var(--radius);
    box-shadow: 0 8px 32px rgba(67, 89, 113, 0.2);
    padding: 0;
    max-width: min(760px, 92vw);
    width: 100%;
    max-height: 88dvh;
    overflow: hidden;
    position: fixed;
    inset: 0;
    margin: auto;
    background: var(--card);
    /* Entry + exit animation via @starting-style - matches the popover
       recipe (see .popover-panel). Base state is hidden (opacity:0 +
       scaled down); [open] upgrades to visible; @starting-style provides
       the FROM values at the moment the dialog opens so the transition
       actually runs instead of snapping. allow-discrete on display +
       overlay coordinates the UA's discrete top-layer flip with the
       opacity/transform animation, so close animates too. */
    opacity: 0;
    transform: scale(0.95);
    transition:
        opacity 0.2s ease-out,
        transform 0.2s ease-out,
        overlay 0.2s ease-out allow-discrete,
        display 0.2s ease-out allow-discrete;
}
dialog[open] {
    opacity: 1;
    transform: scale(1);
}
@starting-style {
    dialog[open] {
        opacity: 0;
        transform: scale(0.95);
    }
}
dialog::backdrop { background: rgba(43, 44, 64, 0.5); animation: backdropIn 0.2s ease-out; }
@keyframes backdropIn { from { opacity: 0; } to { opacity: 1; } }
#image-preview-dialog::backdrop { background: rgba(0, 0, 0, 0.9); }
/* Image preview is a full-viewport native scroll-snap carousel (see the
   .gallery-* rules); it opts out of the centered-card base dialog layout. The
   arrows / close / counter position absolutely against this box. */
#image-preview-dialog {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100dvh;
    max-width: 100vw;
    max-height: 100dvh;
    padding: 0;
    margin: 0;
    border: none;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    overflow: hidden;
}

/* ── Branded confirm dialog (replaces native confirm() via hx-confirm hook) ── */
.confirm-dialog {
    max-width: min(440px, 92vw);
    width: 100%;
    padding: 0;
    border: none;
    border-radius: var(--radius);
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
    background: var(--card);
    color: var(--text);
    overflow: hidden;
}
.confirm-dialog::backdrop { background: rgba(43, 44, 64, 0.55); animation: backdropIn 0.15s ease-out; }
.confirm-dialog .confirm-body {
    display: flex;
    gap: 1rem;
    padding: 1.5rem 1.5rem 1.25rem;
    align-items: center;
}
.confirm-dialog .confirm-icon {
    flex-shrink: 0;
    width: 44px; height: 44px;
    border-radius: var(--radius-full);
    display: flex; align-items: center; justify-content: center;
    background: var(--primary-light);
    color: var(--primary);
}
.confirm-dialog .confirm-icon svg, .confirm-dialog .confirm-icon i { width: 24px; height: 24px; }
/* Show only the icon matching the dialog's mode. Both icons are always present
   in the DOM so lucide.createIcons() runs once at page load; JS just toggles
   the .is-destructive class on the dialog. */
.confirm-dialog .confirm-icon-destructive { display: none; }
.confirm-dialog.is-destructive .confirm-icon-safe { display: none; }
.confirm-dialog.is-destructive .confirm-icon-destructive { display: inline-flex; }
.confirm-dialog.is-destructive .confirm-icon {
    background: var(--danger-strong-tint-stronger);
    color: var(--danger-strong);
}
.confirm-dialog .confirm-message {
    flex: 1;
    min-width: 0;
    margin: 0;
    font-size: var(--text-body);
    font-weight: 500;
    color: var(--heading);
    line-height: 1.45;
}
.confirm-dialog .confirm-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.875rem 1.5rem;
    background: var(--bg);
    border-top: 1px solid var(--border);
}
.confirm-dialog .confirm-actions button {
    min-width: 96px;
    font-size: var(--text-body);
    padding: 0.5rem 1rem;
    margin: 0;
}
/* Destructive variant: the strong-red token automatically adapts to theme
   because --danger-strong / --danger-strong-hover are redefined in the dark
   root. No per-theme override needed here - it "just works" via the variable. */
.confirm-dialog.is-destructive #confirm-ok {
    background: var(--danger-strong);
    border-color: var(--danger-strong);
    color: #fff;
}
.confirm-dialog.is-destructive #confirm-ok:hover {
    background: var(--danger-strong-hover);
    border-color: var(--danger-strong-hover);
}
[data-theme="dark"] .confirm-dialog .confirm-actions { background: var(--input-bg); border-top-color: var(--border); }
/* Modal body content fade-in when loaded */
#popover-body > * { animation: contentFadeIn 0.15s ease-out; }
@keyframes contentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }

/* Native popover panel theme + sizing. Applied to any [popover] element.
   Top-layer rendering comes from the browser's UA stylesheet for [popover]
   (position:fixed; inset:0; width:fit-content). Centering relies on
   margin:auto - restated here because the universal reset above
   (* { margin: 0 }) overrides the UA's margin:auto in author origin.
   https://developer.mozilla.org/en-US/docs/Web/API/Popover_API */
.popover-panel {
    margin: auto;
    padding: 1.5rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
    min-width: min(420px, calc(100vw - 2rem));
    /* Was max-width: min(600px, ...). Most
       popover bodies in this app are forms (entity create/edit) with
       .form-grid auto-fit columns + selects; at 600px max, customer-
       name truncation in selects was common and side-by-side fields
       fought for space. 760px gives forms breathing room without
       per-caller opt-in. The 100vw - 2rem clamp keeps mobile from
       overflowing. */
    max-width: min(760px, calc(100vw - 2rem));
    max-height: calc(100dvh - 4rem);
    overflow-y: auto;
    /* ── Entry / exit animation via @starting-style ──
       The base state (below) is the HIDDEN state; :popover-open upgrades
       to visible. @starting-style (further down) provides the initial
       FROM values at the moment the popover opens, so the transition
       actually runs instead of snapping straight to the open state.
       transition-behavior: allow-discrete on `display` + `overlay` lets
       the UA's discrete display/top-layer flip coordinate with the
       opacity/transform animation. Prefers-reduced-motion zeroes this
       out via the global * { transition-duration: 0.01ms } block. */
    opacity: 0;
    transform: translateY(-6px) scale(0.98);
    transition:
        opacity 0.15s ease-out,
        transform 0.15s ease-out,
        overlay 0.15s ease-out allow-discrete,
        display 0.15s ease-out allow-discrete;
}
.popover-panel:popover-open {
    opacity: 1;
    transform: translateY(0) scale(1);
}
@starting-style {
    .popover-panel:popover-open {
        opacity: 0;
        transform: translateY(-6px) scale(0.98);
    }
}
.popover-panel::backdrop {
    background: rgba(0, 0, 0, 0.45);
    animation: popoverBackdropFade 120ms ease-out;
}
@keyframes popoverBackdropFade {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* ── Toast ── */
.toast-container {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    pointer-events: none;
}
.toast {
    pointer-events: auto;
    padding: 0.75rem 1.25rem;
    border-radius: var(--radius-sm);
    font-size: var(--text-body);
    font-weight: 500;
    box-shadow: var(--shadow-md);
    animation: toast-in 0.3s ease, toast-out 0.3s ease 2.7s forwards;
    max-width: 400px;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.toast-success { background: var(--success); color: var(--white); }
.toast-error { background: var(--danger); color: var(--white); }
.toast-warning { background: var(--warning); color: var(--white); }
.toast-info { background: var(--info); color: var(--white); }
@keyframes toast-in { from { opacity: 0; transform: translateY(-1rem); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { from { opacity: 1; } to { opacity: 0; } }

/* ── Search input styling ── */
input[type="search"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23697A8D' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8' stroke='%23697A8D' stroke-width='2' fill='none'/%3E%3Cpath d='m21 21-4.3-4.3' stroke='%23697A8D' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 0.75rem center;
    padding-left: 2.25rem;
}
[data-theme="dark"] input[type="search"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23aeb0d0' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8' stroke='%23aeb0d0' stroke-width='2' fill='none'/%3E%3Cpath d='m21 21-4.3-4.3' stroke='%23aeb0d0' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* (legacy .fw-600 removed - defined in utility section below) */

/* Activity feed timeline overrides */
#activity-feed .timeline-entry::before { display: none; }

/* ── Activity feed: smooth "Load older activity" transitions ──
   Pairs with loadOlderActivity() in Dashboard/Views/Index.cshtml, which applies:
     .feed-fading-out → on the outgoing button while fetch is in flight
     .feed-new        → on newly-inserted items for one double-rAF so the base
                        transition below picks up when the class is removed.
   Scoped to #activity-timeline so no other feature accidentally inherits these. */
#activity-timeline > .timeline-entry,
#activity-timeline > .feed-load-more {
    transition: opacity var(--transition-slow) ease-out, transform var(--transition-slow) ease-out;
}
/* Out: old button fades + sinks slightly before being removed. */
#activity-timeline > .feed-load-more.feed-fading-out {
    opacity: 0;
    transform: translateY(4px);
    transition: opacity var(--transition-fast) ease-out, transform var(--transition-fast) ease-out;
}
/* In: newly-inserted items paint at this start state, then the class is stripped
   on the next frame and the base transition carries them to their resting state. */
#activity-timeline > .timeline-entry.feed-new,
#activity-timeline > .feed-load-more.feed-new {
    opacity: 0;
    transform: translateY(8px);
    transition: none;
}
/* Honour users who opt out of motion. */
@media (prefers-reduced-motion: reduce) {
    #activity-timeline > .timeline-entry,
    #activity-timeline > .feed-load-more,
    #activity-timeline > .timeline-entry.feed-new,
    #activity-timeline > .feed-load-more.feed-new,
    #activity-timeline > .feed-load-more.feed-fading-out {
        transition: none;
        transform: none;
        opacity: 1;
    }
}

/* ── Chat ── */

/* ── Chat Layout (sidebar + conversation) ── */
.chat-layout { display: grid; grid-template-columns: clamp(200px, 20vw, 320px) 1fr; height: calc(100dvh - var(--topbar-height) - 120px); min-height: 500px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--card); }
.chat-sidebar { border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: var(--card); }
/* Mobile-only back button (hidden on desktop). Shown by the 640px media
   block which forces display:inline-flex. Uses :not(.btn-pill) on button so
   the base button rule doesn't paint it solid primary. */
button.chat-back { display: none; align-items: center; gap: 0.25rem; background: transparent; border: none; color: var(--text-muted); font-size: var(--text-sm); padding: 0.35rem 0.5rem; min-height: 36px; cursor: pointer; }
button.chat-back:hover { color: var(--primary); background: transparent; border: none; }
button.chat-back svg, button.chat-back i[data-lucide] { width: 20px; height: 20px; }
.chat-sidebar-header { padding: 0.75rem; border-bottom: 1px solid var(--border); display: flex; gap: 0.35rem; align-items: center; flex-shrink: 0; }
.chat-sidebar-header input { flex: 1; border-radius: var(--radius-pill); padding: 0.4rem 0.85rem; font-size: var(--text-sm); margin: 0; }
/* Quiet 28px icon buttons. These were <a> tags until the Popover API
   migration required real <button>s (popovertarget) - so the class must
   re-state background/border/padding or the global solid-button theme
   paints them as purple pills (scoped override, no :not() entries). */
.chat-sidebar-btn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; flex-shrink: 0; color: var(--text-muted); background: none; border: 0; border-radius: var(--radius-sm); text-decoration: none; }
.chat-sidebar-btn:hover { color: var(--primary); background: var(--bg); text-decoration: none; border-color: transparent; }
.chat-sidebar-btn svg, .chat-sidebar-btn i[data-lucide] { width: 16px; height: 16px; }
.chat-contact-list { flex: 1; overflow-y: auto; }
.chat-contact { display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 1rem; cursor: pointer; border-left: 3px solid transparent; transition: background var(--transition-fast); }
.chat-contact:hover { background: var(--bg); }
.chat-contact.active { background: var(--primary-light); border-left-color: var(--primary); }
.chat-contact.archived { opacity: 0.5; }
.chat-contact-avatar { width: 36px; height: 36px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-size: var(--text-sm); font-weight: 600; flex-shrink: 0; color: var(--white); }
.chat-contact-info { flex: 1; min-width: 0; }
.chat-contact-name { font-weight: 600; font-size: var(--text-sm); color: var(--heading); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-contact-preview { font-size: var(--text-sm); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 0.1rem; }
.chat-contact-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; flex-shrink: 0; }
.unread-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--radius-pill); background: var(--primary); color: var(--white); font-size: var(--text-xs); font-weight: 700; flex-shrink: 0; }
.chat-section-label { padding: 0.5rem 1rem 0.25rem; font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }

/* Conversation panel */
.chat-main { display: flex; flex-direction: column; overflow: hidden; }
.chat-conv-header { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
.chat-conv-header-left { display: flex; align-items: center; gap: 0.625rem; }
.chat-conv-header-left .chat-contact-avatar { width: 32px; height: 32px; font-size: var(--text-xs); }

/* Messages */
.chat-messages { flex: 1; overflow-y: auto; padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: 0.5rem; }
.msg-row { display: flex; gap: 0.625rem; max-width: 75%; }
.msg-row.own { margin-left: auto; flex-direction: row-reverse; }
.msg-avatar { width: 32px; height: 32px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-size: var(--text-xs); font-weight: 600; flex-shrink: 0; background: var(--primary-light); color: var(--primary); }
.msg-row.own .msg-avatar { background: var(--success-light); color: var(--success); }
.msg-bubble { background: var(--bg); border-radius: 12px 12px 12px 4px; padding: 0.5rem 0.875rem; font-size: var(--text-body); line-height: 1.5; word-break: break-word; }
.msg-row.own .msg-bubble { background: var(--primary); color: var(--white); border-radius: 12px 12px 4px 12px; }
.msg-meta { font-size: var(--text-xs); color: var(--text-muted); margin-top: 0.15rem; }
.msg-row.own .msg-meta { text-align: right; }

/* Portal message thread (shared _PortalMessages partial - Customer / Estimate /
   Invoice / Job detail Portal tabs). Token-driven backgrounds so bubbles flip
   in dark mode; the old inline styles resolved undefined vars to fixed light
   hex, leaving light text on a light bubble. Rounded corners follow the chat
   .msg-bubble idiom (kept curved in the sharp theme). */
.pmsg-row { display: flex; }
.pmsg-row.is-customer { justify-content: flex-start; }
.pmsg-row.is-business { justify-content: flex-end; }
.pmsg-bubble { max-width: 80%; padding: 0.75rem 1rem; color: var(--text); }
.pmsg-row.is-customer .pmsg-bubble { background: var(--primary-light); border-radius: 12px 12px 12px 4px; }
.pmsg-row.is-business .pmsg-bubble { background: var(--bg); border-radius: 12px 12px 4px 12px; }

/* Chat input */
.chat-input { padding: 0.75rem 1.25rem; border-top: 1px solid var(--border); flex-shrink: 0; }
.chat-input form { display: flex; gap: 0.5rem; width: 100%; margin: 0; }
.chat-input input { flex: 1; border-radius: var(--radius-pill); padding: 0.5rem 1rem; margin: 0; }
.chat-input button[type="submit"] { border-radius: var(--radius-full) !important; width: 38px !important; height: 38px !important; padding: 0 !important; display: flex !important; align-items: center; justify-content: center; flex-shrink: 0; gap: 0 !important; }

/* Chat composer: attach button + selected-file chip */
.chat-attach-btn { border: none; background: transparent; color: var(--text-muted); width: 38px; height: 38px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0; cursor: pointer; }
.chat-attach-btn:hover { background: var(--bg); color: var(--primary); }
.chat-file-chip { display: flex; align-items: center; gap: 0.4rem; width: fit-content; max-width: 100%; margin-bottom: 0.5rem; padding: 0.3rem 0.5rem 0.3rem 0.6rem; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-pill); font-size: var(--text-sm); color: var(--text); }
.chat-file-chip[hidden] { display: none; }
.chat-file-chip > span { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-file-remove { border: none; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.15rem; }
.chat-file-remove:hover { color: var(--danger); }

/* Chat message attachments (images render as thumbnails, docs as file chips) */
.msg-attachments { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.35rem; }
.msg-row.own .msg-attachments { justify-content: flex-end; }
.msg-attach-img { display: block; max-width: 220px; border-radius: var(--radius); overflow: hidden; cursor: pointer; }
.msg-attach-img img { display: block; width: 100%; max-height: 200px; object-fit: cover; }
.msg-attach-file { display: inline-flex; align-items: center; gap: 0.4rem; max-width: 240px; padding: 0.45rem 0.7rem; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); font-size: var(--text-sm); color: var(--text); text-decoration: none; }
.msg-attach-file:hover { border-color: var(--primary); color: var(--primary); }
.msg-attach-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Per-message edit/delete controls (shown on the last message, on hover) */
.msg-actions { display: flex; align-items: center; gap: 0.15rem; align-self: center; opacity: 0; transition: opacity 0.12s; }
.msg-row:hover .msg-actions, .msg-actions:focus-within { opacity: 1; }
.msg-action { border: none; background: transparent; color: var(--text-muted); cursor: pointer; width: 26px; height: 26px; padding: 0; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; }
.msg-action:hover { background: var(--bg); color: var(--heading); }
.msg-action.danger:hover { color: var(--danger); }
.msg-action i, .msg-action svg { width: 15px; height: 15px; }
.msg-edit-form { display: flex; flex-direction: column; gap: 0.3rem; min-width: min(260px, 60vw); margin: 0.15rem 0; }
.msg-edit-input { width: 100%; margin: 0; border-radius: var(--radius); font-size: var(--text-body); resize: vertical; }
.msg-edit-actions { display: flex; gap: 0.3rem; }

/* ── Leads Pipeline ── */
.pipeline {
    display: flex;
    gap: 0.75rem;
    overflow-x: auto;
    padding-bottom: 1rem;
    min-height: 400px;
    /* Snap column-by-column so the horizontal scroll feels deliberate, not
       like content fell off the edge. */
    scroll-snap-type: x proximity;
}
.pipeline-col {
    /* Columns shrink toward 160px on laptop widths so the core six still fit
       without horizontal scroll on a ~1366 screen; wider viewports let flex
       grow them to fill, genuinely narrow ones scroll (with snap). */
    min-width: clamp(160px, 12vw, 200px);
    flex: 1;
    display: flex;
    flex-direction: column;
    scroll-snap-align: start;
}
.pipeline-header {
    padding: 0.6rem 0.75rem;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    font-size: var(--text-sm);
    font-weight: 600;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.pipeline-header .count {
    background: rgba(255,255,255,0.3);
    padding: 0.1rem 0.5rem;
    border-radius: var(--radius-lg);
    font-size: var(--text-sm);
}
.pipeline-body {
    background: var(--bg);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    padding: 0.5rem;
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.pipeline-card {
    background: var(--card);
    border-radius: var(--radius-sm);
    padding: 0.75rem;
    box-shadow: 0 1px 3px rgba(0,0,0,0.06);
    cursor: pointer;
    transition: box-shadow var(--transition-base);
}
.pipeline-card:hover { box-shadow: 0 3px 8px rgba(0,0,0,0.1); }
.pipeline-card a { text-decoration: none; color: inherit; }
.pipeline-card .lead-name { font-weight: 600; font-size: var(--text-body); color: var(--heading); }
.pipeline-card .lead-meta { font-size: var(--text-sm); color: var(--text); margin-top: 0.25rem; }
.pipeline-card .lead-value { font-size: var(--text-sm); font-weight: 600; color: var(--primary); margin-top: 0.35rem; }

/* Drag-and-drop on the Kanban: cards lift slightly + fade while held, drop
   zones get a primary-color outline + tinted background while hovered. The
   "Drop here" placeholder is hidden once the column has any cards (so empty
   columns advertise the gesture but populated ones don't waste vertical
   space). Dispatch board uses an analogous .dragging / .drag-over pair on
   .dispatch-job - keeping the visual language consistent across the two
   drag-drop surfaces. */
.pipeline-card { cursor: grab; }
.pipeline-card.dragging { opacity: 0.45; cursor: grabbing; transform: rotate(-1deg); }
.pipeline-body.drag-over {
    outline: 2px dashed var(--primary);
    outline-offset: -2px;
    background: var(--primary-light);
}
.pipeline-empty {
    text-align: center;
    padding: 1rem;
    color: var(--text);
    font-size: var(--text-sm);
    opacity: 0.5;
    pointer-events: none;
}
/* After an optimistic drop the placeholder is still in the DOM next to the
   newly-appended card until the server-driven reload arrives. Hide it as
   soon as the column has any cards so the user doesn't see "Drop here"
   under a dropped card for the few hundred ms in flight. */
.pipeline-body:has(.pipeline-card) .pipeline-empty { display: none; }

/* ── Info card - shared per-record card surface ──
   Used app-wide via the <info-card> tag helper. Replaces the
   recurring hand-written "<div class='card' style='position:
   relative;'> ... corner badge ... title ... body ... action footer"
   shape from Customer Sites, Contacts, KB List, Routes, Attachments,
   etc. Uniform padding / radius / corner badge anchoring; freeform
   body via the tag's child slot.

   Cards are split into THREE vertical zones so cards align cleanly
   WITHIN a row (the body absorbs the height variance, footers line up):

       ┌─────────────────────────────────┐
       │ TITLE   (--card-title-lines)    │  ← clamped to N lines
       │                                 │     (default 2). Reserves N
       │                                 │     lines so adjacent cards
       │                                 │     in a row stay aligned.
       ├─────────────────────────────────┤
       │ BODY    (flex:1, overflow hidden)│ ← grows to fill remaining
       │ ...                             │     vertical space; clips
       │                                 │     overflow rather than
       │                                 │     pushing the footer down.
       ├─────────────────────────────────┤
       │ FOOTER  (auto, anchored bottom) │ ← .info-card-actions
       └─────────────────────────────────┘     content-driven height

   The card sizes to its content; the grid's align-items:stretch default
   makes same-row cards equal height (no JS). Override --card-title-lines
   per usage if a list needs taller/shorter titles. */
.info-card {
    /* Three-zone responsive contract:
         HEAD   - fixed N lines (--card-title-lines) via min-height
                  + line-clamp. Reserved space ensures body starts
                  at the same Y across all cards in a row even when
                  some titles are 1 line and others are 2.
         BODY   - flex: 1 1 auto, expands to consume leftover
                  vertical space. Cards in the same grid row stretch
                  to match each other (CSS-grid default), so body
                  expansion delivers visually-even spacing per row.
                  Content sits at body top; leftover space is the
                  bottom gap above the footer.
         FOOTER - fixed --card-footer-min-height (single line),
                  overflow:hidden so multiline content collapses to
                  one. Holds meta OR kebab OR action buttons. Same
                  height across cards.
       Net effect: title and footer baselines align across the whole
       grid; body expansion absorbs the variance. */
    --card-title-lines: 2;
    --card-title-line-height: 1.3;
    --card-footer-min-height: 1.75rem;

    /* The card is its own inline-size container - same pattern that
       makes <kpi-card> scale so cleanly. Internals (title font size,
       padding, icon scale) read against the card's OWN width via cqw,
       so a card in a narrow lane feels right without any media query
       plumbing. The .info-card-grid uses .main-body's container for
       column count; this one is for per-card internals. */
    container-type: inline-size;
    container-name: infocard;

    position: relative;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* The grid track is 1fr (fills the row), but a lone or sparse card would then
       stretch into a wide bar. Cap the CARD at a card width so it keeps its
       silhouette - dense rows still fill (their tracks are narrower than this),
       only an over-wide single card is reined in. */
    max-width: 22rem;
    /* Padding scales lightly with card width - kpi-card pattern. */
    padding: clamp(0.75rem, 0.5rem + 0.6cqw, 1rem) clamp(0.875rem, 0.6rem + 0.7cqw, 1.125rem);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: box-shadow var(--transition-base), transform var(--transition-base);
    display: flex;
    flex-direction: column;
    /* Height: none. The card sizes to its content. Equal height WITHIN a row
       is the CSS-grid default (the grid item stretches to its row's height via
       align-items:stretch), and the body's flex:1 + footer margin-top:auto keep
       footers aligned across that row. Cross-row uniform height is deliberately
       NOT pursued - it's the one thing pure CSS can't do for an auto-height
       container, and chasing it with a JS measure/pin pass (the old
       equalizeInfoCards) both skipped single-card grids - so a lone Customer
       contact ballooned to the full detail column - and re-ran on every swap /
       resize. Same model as .kpi-card: content-sized, container-query internals,
       grid handles the rest. */
    overflow: hidden;
}
.info-card:hover {
    /* Lift on hover, mirroring the kpi-card affordance. */
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
    border-color: color-mix(in srgb, var(--primary) 25%, var(--border));
}
/* Compact variant - when the card itself is under 12rem wide (dense
   grids, narrow detail panels), tighten padding + title font + zone
   gaps so the card stays legible at a glance. Same pattern kpi-card
   uses below 200px. Driven by the card's OWN container, not the
   viewport, so it works wherever the card gets HTMX-swapped to. */
@container infocard (max-width: 12rem) {
    .info-card {
        padding: 0.6rem 0.75rem;
        /* Tighter body in compact lanes since less title/footer
           context is available, but still uniform across cards. */
        --card-body-height: 2.75rem;
    }
    .info-card-title { margin-bottom: 0.35rem; }
    .info-card-actions { padding-top: 0.5rem; }
}
.info-card-badge {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
}
.info-card-title {
    /* Zone 1: title - locked to --card-title-lines lines.
       Font size uses cqw (1cqw = 1% of the card's own inline size)
       so the title scales with the card itself, like kpi-value does
       for the headline number. clamp() keeps it readable at both
       extremes - never below 0.875rem on tight lanes, never above
       1.05rem on wide cards. min-height reserves N lines of
       vertical space even for short (1-line) titles so titles +
       bodies + footers all align across sibling cards. */
    margin: 0 0 0.5rem;
    /* Title sized smaller than the previous 0.875-1.05rem range - at
       typical KB tile widths the prior clamp landed around 14-15px
       which felt heavy next to the body badges and footer meta.
       New range floors at 0.8rem (~12.8px) and tops out at 0.92rem
       (~14.7px) on wide cards. cqw still drives the scale so the
       title grows with the card, just at a tamer slope. */
    font-size: clamp(0.8rem, 0.65rem + 1cqw, 0.92rem);
    font-family: var(--font-display);
    color: var(--heading);
    font-weight: 600;
    line-height: var(--card-title-line-height);
    letter-spacing: -0.01em;
    /* Reserve room on the right so the title doesn't slide under the
       absolute-positioned badge. When no badge renders the padding is
       harmless. */
    padding-right: 4.5rem;
    flex-shrink: 0;
    display: -webkit-box;
    -webkit-line-clamp: var(--card-title-lines);
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
    min-height: calc(var(--card-title-line-height) * var(--card-title-lines) * 1em);
}
/* Title row: a flex line that holds the type-glyph icon and the title.
   Keeping them as siblings (vs. icon nested inside the title) means the
   title's -webkit-box line-clamp clamps the TEXT, not the icon, and we
   can use a normal flex `gap` for the spacing instead of margin hacks. */
.info-card-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    min-width: 0;
}
.info-card-head > .info-card-title { margin: 0 0 0.5rem; flex: 1 1 auto; }

/* Type-glyph icon next to the title. Tinted with primary at low alpha
   so it reads as the card's "what kind am I?" signal without competing
   with the title text. */
.info-card .info-card-icon {
    width: 1rem;
    height: 1rem;
    color: var(--primary);
    opacity: 0.7;
    flex-shrink: 0;
    transform: translateY(0.15rem); /* optical centre with the title cap-height */
    transition: opacity var(--transition-base) ease, color var(--transition-base) ease, transform var(--transition-base) ease;
}
.info-card:hover .info-card-icon { opacity: 1; }
/* Placeholder modifier when the underlying data has no title. Italic +
   muted reads as "this is a fallback, not a real title" without breaking
   the card layout. */
.info-card-title.is-untitled {
    font-style: italic;
    color: var(--text-muted);
    font-weight: 500;
}
.info-card-body {
    /* Zone 2: body - flex:1 expands to consume any vertical space
       left over after title (fixed) and footer (fixed) are placed.
       Cards in the same grid row stretch to match each other (CSS-grid
       align-items:stretch default), so the body expands per-card to keep
       titles + footers aligned across that row.

       display:flex column matters: the InfoCardTagHelper wraps ALL
       child slot content in this body element (including the
       caller-provided .info-card-actions footer), so the footer
       lives INSIDE body, not as a sibling. Making body a flex
       column lets the footer's margin-top:auto push it to the
       bottom of body - without this, the footer sits right after
       the tags with empty space below it, breaking the 3-zone
       contract.

       min-height:0 + overflow:hidden let long content clip rather
       than push the footer down. */
    color: var(--text);
    font-size: var(--text-body);
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}
.info-card-actions {
    /* Zone 3: footer - fixed single-line height via
       --card-footer-min-height + overflow:hidden. Holds either a
       meta row, a kebab menu, or action buttons. Same height on
       every card so footers align at a uniform Y from the card
       bottom. justify-content:flex-end parks contents at the
       trailing edge - the conventional bottom-right slot for a
       lone kebab or action. flex-shrink:0 keeps it from collapsing
       when body content runs long. */
    flex-shrink: 0;
    flex-wrap: nowrap;
    min-height: var(--card-footer-min-height);
    overflow: hidden;
    padding-top: 0.5rem;
    display: flex;
    gap: 0.5rem;
    align-items: center;
    justify-content: flex-end;
}
/* No info-card-specific kebab override - the universal
   .actions-trigger style above (outline-primary) renders the same
   on every surface (table rows, page headers, card footers) so the
   kebab pattern feels consistent app-wide. */
/* Grid container - canonical layout for stacks of info-cards.
   Fluid track: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)).
     - floor 14rem sets the breakpoint: ~1 col under 14rem, +1 col per
       ~14rem of width (2 cols ~28rem, 3 ~42rem, ...).
     - 1fr lets the columns expand to FILL the row, so cards grow into the
       available space instead of capping at a fixed width and leaving a
       gutter. The min(100%, ...) collapses to a single full-width column
       on a container narrower than the floor.
   No JS. Columns fill the row (1fr); a lone/sparse card is kept card-shaped
   by .info-card's own max-width, not by capping the track. Card height sizes
   to content (see .info-card). */
.info-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
    gap: clamp(0.5rem, 1cqi, 1.25rem);
}
/* Stack variant - vertical column for cases where each card is wide
   (Customer Contacts) and a grid would chop content awkwardly. */
.info-card-stack {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

/* ── Phase card variant ──
   Job phases use the unified info-card chrome (grid layout for
   squarish tiles, container-aware row packing) and keep the vibrant
   status accent (colored left border + 8% background tint) that
   communicates progress at a glance. The body uses flex:1 to
   expand and absorb content variance, so no per-context body
   height override is needed - the universal contract applies. */
.phase-card {
    /* The 4px left border replaces the .info-card 1px left border.
       Right/top/bottom keep the neutral 1px so the card still has
       defined edges on three sides regardless of status. */
    border-left-width: 4px;
}
.phase-card.status-pending      { border-left-color: var(--border); }
.phase-card.status-inprogress {
    border-left-color: var(--info);
    background: color-mix(in srgb, var(--info) 8%, var(--card));
}
.phase-card.status-completed {
    border-left-color: var(--success);
    background: color-mix(in srgb, var(--success) 8%, var(--card));
}
.phase-card.status-skipped {
    border-left-color: var(--warning);
    background: color-mix(in srgb, var(--warning) 8%, var(--card));
}
/* Sort number micro-label - small, right-aligned at the top under the
   corner badge. Uses absolute positioning so it doesn't disturb the
   3-zone hand-off. */
.phase-card__sort {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    font-size: var(--text-xs);
    /* Sit BELOW the corner badge - the badge already absolute-positions
       at top:0.5rem right:0.5rem. Push the sort number down past the
       badge height + a small gap. When no badge renders this is just
       a small offset from the corner, harmless. */
    transform: translateY(1.5rem);
}
.phase-card__meta {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    margin-top: 0.35rem;
}
.phase-card__meta > span {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}
/* Whole-card link wrapper - used when the entire info-card surface
   should navigate (KB list, etc.). Strips link chrome and acts as a
   flex passthrough so the inner .info-card stretches to fill the
   grid-row height (equal-height cards in a row). */
.info-card-link {
    display: flex;
    text-decoration: none;
    color: inherit;
    cursor: pointer;   /* visible affordance - these cards open something */
}
.info-card-link > .info-card { flex: 1 1 auto; }
/* Beat the global `a:not(...):hover { text-decoration: underline }`
   specificity (0,3,1) so the whole-card click target doesn't get
   underlined on hover. Same :not chain ties specificity; mine wins
   because help-wiki / call sites load after app.css. */
a.info-card-link:not([role="button"]):not(.ghost):hover,
a.info-card-link:not([role="button"]):not(.ghost):focus-visible {
    text-decoration: none;
}
/* Clickable affordance - lift, primary-tinted border, stronger shadow,
   and a title color shift on hover/focus tell the user the whole card
   is the click target (not just the title text). Scoped to .info-card-link
   so non-clickable .info-card uses (Customer Sites, Vertical Packs,
   etc. when used as static surfaces) don't grow a hover state they
   don't deserve. The transform + box-shadow transitions are already
   declared on .info-card, so this just provides the values they animate to. */
.info-card-link:hover > .info-card,
.info-card-link:focus-visible > .info-card {
    border-color: var(--primary);
    box-shadow: 0 10px 28px -8px color-mix(in srgb, var(--primary) 40%, transparent),
                0 2px 6px rgba(0,0,0,0.04);
    transform: translateY(-2px);
}
.info-card-link:hover .info-card-title,
.info-card-link:focus-visible .info-card-title {
    color: var(--primary);
}
/* The icon picks up primary at full opacity on hover/focus so it
   tracks the title's colour shift instead of sitting behind. */
.info-card-link:hover .info-card-icon,
.info-card-link:focus-visible .info-card-icon {
    opacity: 1;
    color: var(--primary);
    transform: scale(1.08);
}
/* Pressed state: card sinks back slightly so the click feels tactile. */
.info-card-link:active > .info-card {
    transform: translateY(0);
    box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent);
    transition-duration: 60ms;
}
/* Focus-visible ring on the link wrapper, drawn around the card with a
   small offset so the primary border + the ring don't collide. The
   card itself owns the lift; the ring owns the keyboard-focus signal. */
.info-card-link:focus { outline: none; }
.info-card-link:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 3px;
    border-radius: calc(var(--radius) + 3px);
}

.info-card-tags {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    flex: 1;
    min-width: 0;
    /* Default align-items: stretch made single badges grow vertically to
       fill the card body when sibling cards in the row were taller (e.g.
       Tags card with many badges next to Industries card with one). Pin
       to flex-start so badges keep their natural height regardless of
       container height. */
    align-items: flex-start;
    align-content: flex-start;
}


/* Signature image inside an info-card body. White bg + thin border so the
   captured stroke (transparent PNG with dark ink) reads cleanly regardless
   of the active theme. */
.signature-preview {
    width: 100%;
    height: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: #fff;
}

/* ── Pipeline scaling controls ──
   Three layers that keep the board scannable as data grows:
   1) Server-side time-window filter (default 90 days, see LeadEndpoints).
   2) Per-column visible-card cap (rendered in DOM so drag-drop stays
      consistent, hidden via CSS until "Show all" toggles .is-expanded).
   3) Aging color tiers on cards - visual prioritization for stuck cards
      so the eye lands on what needs attention without filtering anything
      out. Terminal stages (Won / Lost / Abandoned / Disqualified) skip
      the tier in the Razor view since closed leads aren't "stale". */
.pipeline-col:not(.is-expanded) .pipeline-card:nth-of-type(n+11) {
    display: none;
}
.pipeline-show-all {
    background: transparent;
    border: 1px dashed var(--border);
    color: var(--text-muted);
    padding: 0.4rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    cursor: pointer;
    width: 100%;
    transition: var(--transition-btn);
}
.pipeline-show-all:hover {
    border-color: var(--primary);
    color: var(--primary);
    background: var(--primary-light);
}

/* Aging tiers - left-border accent. Subtle on aging (amber), stronger
   on stale (red). The base .pipeline-card has no left-border so these
   rules just paint the accent without affecting layout / size. */
.pipeline-card[data-age-tier="aging"] {
    border-left: 3px solid var(--warning);
    padding-left: calc(0.75rem - 3px);
}
.pipeline-card[data-age-tier="stale"] {
    border-left: 3px solid var(--danger);
    padding-left: calc(0.75rem - 3px);
}

/* ══════════════════════════════════════════
   Dashboard
   ══════════════════════════════════════════ */
.dashboard-row { display: grid; gap: 1.25rem; margin-bottom: 1.25rem; }
.dashboard-row:last-child { margin-bottom: 0; }

/* Generic card used by Dashboard (stat cards use shared .kpi-card).
   NOTE: we intentionally do NOT set overflow:hidden here - Frappe Charts
   tooltips are absolutely-positioned inside the chart and need to escape
   the card bounds when they appear near the top/edges. No child actually
   overflows the rounded corners in normal use, so border-radius still
   clips correctly without needing overflow:hidden. */
.dashboard-card {
    background: var(--card);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    transition: box-shadow var(--transition-base) ease;
}
.dashboard-card:hover { box-shadow: var(--shadow-card-hover); }

/* Dashboard panel headers (chart / summary / activity-feed) share one h4 style */
.chart-header h4, .summary-header h4, .feed-header h4 { margin: 0; font-size: var(--text-body); font-weight: 600; color: var(--heading); }

/* Chart card */
.chart-header { padding: 1.25rem 1.25rem 0; position: relative; }
.chart-header p { margin: 0.15rem 0 0; font-size: var(--text-sm); color: var(--text-muted); }
/* Right-aligned "View ->" jump from a chart card to its full list view. */
.chart-header-link { position: absolute; top: 1.25rem; right: 1.25rem; font-size: var(--text-sm); white-space: nowrap; }
.chart-body { padding: 0.5rem 0.75rem 0.75rem; }

/* Summary list */
.summary-header { padding: 1.25rem 1.25rem 0.5rem; }

/* Activity feed */
.feed-header {
    padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-light);
    display: flex; justify-content: space-between; align-items: center;
}
.feed-body { padding: 0.5rem 1.25rem 0.75rem; overflow-y: auto; max-height: 460px; }
/* Empty-state wrapper for the dashboard activity feed. Centered text
   on an existing .p-xl spacing. Replaces inline
   style="text-align:center; color:var(--text);" on the empty-state div. */
/* "Load older activity" CTA strip at the bottom of the timeline.
   Layout properties used to live inline; promoting them here keeps
   the cshtml class-only and lets dark-mode + responsive overrides
   reach a stable selector. */
.feed-load-more {
    text-align: center;
    padding: 0.75rem 0;
    border-top: 1px solid var(--border);
}
/* Activity-feed dot. Positions a colored circle on the timeline's
   left rail so the dot can take a data-driven background-color
   (var(--success|warning|danger|info|primary)) without inlining
   width/height/position/border alongside it. Pair with an inline
   `style="background:@dotColor;"` for the only dynamic property. */
.timeline-dot {
    position: absolute;
    left: -1.375rem;
    top: 0.2rem;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid var(--card, var(--white));
}
/* 24×24 muted icon tile used in the activity feed alongside each entry.
   Centers a Lucide icon-sm inside a rounded square with the page-bg
   fill. Replaces the recurring inline compound
   style="width:24px;height:24px;border-radius:var(--radius-sm);
   flex-shrink:0;background:var(--bg);color:var(--text);". */
/* Icon tile; a square, centered glyph on a tinted surface. One reusable
   scale: .icon-tile (prominent, header-sized, fluid) + .icon-tile-xs (compact
   row glyph). Tint with .primary. The inner glyph sizes off the tile (%), so a
   tile resized at any breakpoint never needs a separate glyph rule. */
.icon-tile, .icon-tile-xs {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
}
.icon-tile-xs { width: 24px; height: 24px; }
.icon-tile {
    width: clamp(42px, 2.4rem + 1.2vw, 52px);
    height: clamp(42px, 2.4rem + 1.2vw, 52px);
    border-radius: var(--radius-lg);
}
.icon-tile > :is(i, svg) { width: 54%; height: 54%; }
.icon-tile.primary { background: var(--primary-light); color: var(--primary); }


.progress-fill { height: 100%; border-radius: var(--radius-xs); }


/* .grid-2 is defined mobile-first in the Grid Utilities block (single column,
   auto-fit two-up at >=640px). The old `.grid-2 { 1fr 1fr }` orphan that lived
   here forced two columns at every width and was removed - it overflowed
   two-column detail blocks on phones. */


/* ══════════════════════════════════════════
   Dispatch Board
   ══════════════════════════════════════════ */
.dispatch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
    gap: 1rem;
}
.team-lane {
    container-type: inline-size;
    container-name: lane;
    background: var(--card);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    display: flex;
    flex-direction: column;
    min-height: 300px;
}
/* Narrow lane (tight dispatch grids, sidebar embeds) - tighten inner density.
   Triggers on lane width, not viewport, so a lane squeezed to 260px in a
   4-col grid on a wide monitor still tightens up. */
@container lane (max-width: 300px) {
    .dispatch-job { padding: 0.5rem 0.625rem; font-size: var(--text-sm); }
    .dispatch-job .job-num { font-size: var(--text-sm); }
    .team-header { padding: 0.75rem 0.875rem; }
}
.team-header {
    padding: 0.875rem 1rem;
    border-bottom: 1px solid var(--border-light);
    display: flex;
    align-items: center;
    gap: 0.625rem;
}
.worker-avatar {
    width: 32px; height: 32px; border-radius: var(--radius-full);
    display: flex; align-items: center; justify-content: center;
    color: #fff; font-weight: 600; font-size: var(--text-sm); flex-shrink: 0;
}
/* Gap bumped from 0.5rem to 0.625rem so the
   dispatch-job cards in a tech lane don't visually melt when 3+
   stack. At --text-sm body, 0.5rem felt like the cards were touching. */
.worker-jobs { padding: 0.75rem; flex: 1; display: flex; flex-direction: column; gap: 0.625rem; }
.dispatch-job {
    padding: 0.625rem 0.75rem;
    border-radius: var(--radius-sm);
    border-left: 3px solid;
    background: #fafafa;
    font-size: var(--text-sm);
    transition: box-shadow var(--transition-base);
}
.dispatch-job:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.dispatch-job .job-num { font-weight: 600; color: var(--heading); }
.dispatch-job .job-cust { color: var(--text); margin-top: 0.15rem; }
.dispatch-job .job-time { color: var(--text-muted); font-size: var(--text-xs); margin-top: 0.2rem; }
.dispatch-job .job-addr { color: var(--text-muted); font-size: var(--text-xs); }

.unassigned-lane { border: 2px dashed var(--border); border-radius: var(--radius-card); }
.unassigned-lane .team-header { border-bottom: 2px dashed var(--border); }

.day-nav { display: flex; align-items: center; gap: 0.75rem; }
.day-nav a { text-decoration: none; color: var(--primary); font-size: var(--step-3); padding: 0.25rem 0.5rem; }

.empty-lane { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: var(--text-sm); opacity: 0.5; }
/* Sub-section header inside a tech's lane separating
   "Pending schedule" (unscheduled but assigned) from "Scheduled". */
.lane-section-header {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    color: var(--text-muted);
    padding: 0.4rem 0.25rem 0.2rem;
    display: flex;
    align-items: center;
    gap: 0.3rem;
    border-bottom: 1px dashed var(--border);
    margin-bottom: 0.25rem;
}
.lane-section-header + .lane-section-header { margin-top: 0.5rem; }

/* Drag-and-drop states */
.dispatch-job[draggable="true"] { cursor: grab; }
.dispatch-job[draggable="true"]:active { cursor: grabbing; }
.dispatch-job.dragging { opacity: 0.4; transform: scale(0.96); }
.worker-jobs.drag-over { background: rgba(105,108,255,0.08); border-radius: 0 0 10px 10px; outline: 2px dashed rgba(105,108,255,0.3); outline-offset: -2px; }
.team-lane-timeoff { opacity: 0.6; }
.team-lane-timeoff .team-header { background: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(0,0,0,0.02) 10px, rgba(0,0,0,0.02) 20px); }
.badge-timeoff { background: color-mix(in srgb, var(--warning) 12%, transparent); color: var(--warning); font-size: var(--text-xs); padding: 0.1rem 0.4rem; border-radius: var(--radius-xs); font-weight: 600; }

/* ══════════════════════════════════════════
   Map View (Leaflet)
   ══════════════════════════════════════════ */
.map-layout { display: flex; gap: 1rem; height: calc(100dvh - 140px); }
.map-sidebar {
    /* flex-basis 340px (vs. width: 340px) swaps axes when the parent flips to
       column-reverse at 768px - cross-axis width becomes auto and stretches to
       fill, so the mobile override only needs max-height, not width. */
    flex: 0 0 340px; overflow-y: auto;
    background: var(--card); border-radius: var(--radius-card); box-shadow: var(--shadow-card);
}
.map-container {
    flex: 1; border-radius: var(--radius-card); overflow: hidden;
    box-shadow: var(--shadow-card);
}
#leaflet-map { height: 100%; width: 100%; }
.map-sidebar-header { padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); }
.map-sidebar-header h3 { margin: 0; font-size: var(--step-2); }
.map-job-item {
    padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border-light);
    cursor: pointer; transition: background var(--transition-base) ease;
}
.map-job-item:hover { background: var(--bg); }
.map-job-item .job-num { font-weight: 600; font-size: var(--text-sm); color: var(--heading); }
/* job-cust (customer name) is the second-most important field on the
   row after the job number - bump from --text-muted (~5.5:1 contrast
   in dark mode, technically passes AA but feels washed out) to --text
   and add weight so the eye finds it without squinting. */
.map-job-item .job-cust { font-size: var(--text-sm); color: var(--text); font-weight: 500; margin-top: 0.15rem; }
.map-job-item .job-addr { font-size: var(--text-xs); color: var(--text-muted); margin-top: 0.15rem; }
.map-job-item .job-time { font-size: var(--text-xs); color: var(--primary); font-weight: 500; }
/* job-teamMembers (assigned tech) - same readability nudge as job-cust.
   Tech name is workflow-critical (the dispatcher routes to it) and was
   blending into the address line above it in dark mode. */
.map-job-item .job-teamMembers { font-size: var(--text-xs); color: var(--text); font-weight: 500; }


.no-geocode-note {
    padding: 0.5rem 1rem; margin: 0 1.25rem 0.75rem; border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--warning) 10%, transparent); color: var(--warning); font-size: var(--text-xs);
}

/* ══════════════════════════════════════════
   Reports
   ══════════════════════════════════════════ */
.report-section {
    background: var(--card); border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    margin-bottom: 1.5rem;
    /* overflow:hidden removed so chart tooltips can escape the section bounds. */
}
.report-section-header {
    padding: 1.25rem 1.5rem 1rem;
    display: flex; align-items: center; gap: 0.75rem;
    border-bottom: 1px solid var(--border-light);
}
.report-section-header .section-icon {
    width: 38px; height: 38px; border-radius: var(--radius);
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.report-section-header .section-icon svg { width: 20px; height: 20px; }
.report-section-header h3 { margin: 0; font-size: var(--step-2); font-weight: 600; color: var(--heading); }
.report-section-header .section-description { font-size: var(--text-sm); color: var(--text-muted); margin-top: 0.1rem; }
.report-section-body { padding: 1.25rem 1.5rem; }

/* KPI mini grid for reports */
.report-kpis {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
    gap: 1rem;
}

/* ── KPI strip mode ─────────────────────────────────────────────────
   Adds the .kpi-strip modifier alongside any KPI grid wrapper to
   reshape the kpi-card children as flat stat-chip-style chips.
   Single CSS class flip - no tag helper or markup changes - so
   drill-down click handlers and Alpine customize-id wiring still
   work. Used on Reports' 19 section KPI grids and Dashboard Row 2
   (customizable extras) to recover ~400-600px of vertical chrome
   per page. Row 1 hero tiles stay full-size by omitting the class.

   Layout mirrors .dashboard-stats-strip .stat-chips (used by the
   admin Dashboard): grid auto-fit with cqi-based fluid minmax so
   chips distribute evenly across the row's width and shrink + grow
   with the container, not the viewport. container-type: inline-size
   on the wrapper enables the cqi units inside. */
.kpi-strip {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.7rem 1rem;
    margin-bottom: 0.75rem;

    /* Auto-fit grid with a 7rem floor so chips lay out as 6-9 cols at
       desktop, ~4 at tablet, 2-3 at mobile - reflowing with the
       container, not the viewport. Specificity beats .report-kpis on
       cascade order (both are single-class selectors, later wins).
       cqi would scale even more smoothly with container width, but
       it'd require an outer wrapper with container-type around every
       .kpi-strip callsite - markup tax not worth the polish here. */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 7rem), 1fr));
    gap: 0.6rem 1.25rem;
    align-items: baseline;
}

/* In strip mode, float the NOW pill to the chip's right edge so it
   balances the label / value column instead of crowding the label. */
.kpi-strip .kpi-now {
    float: right;
    margin: 0;
}

/* Drill-down affordance - faint right-edge primary tick + chevron-on-
   hover. Makes the clickable chips visually distinct from static ones
   without adding loud chrome. */
.kpi-strip .kpi-card--clickable {
    padding-right: 0.85rem;
    border-right: 2px solid color-mix(in srgb, var(--primary) 25%, transparent);
    transition: border-color var(--transition-base) ease, color var(--transition-base) ease;
}
.kpi-strip .kpi-card--clickable::after {
    content: "›";
    position: absolute;
    top: 50%;
    right: 0.15rem;
    transform: translateY(-50%);
    color: color-mix(in srgb, var(--primary) 45%, transparent);
    font-size: 1rem;
    line-height: 1;
    transition: color var(--transition-base) ease, transform var(--transition-base) ease;
    pointer-events: none;
}
.kpi-strip .kpi-card--clickable:hover {
    border-right-color: var(--primary);
}
.kpi-strip .kpi-card--clickable:hover::after {
    color: var(--primary);
    transform: translateY(-50%) translateX(2px);
}
/* The clickable chip needs position:relative so the ::after chevron
   anchors to the chip and not some ancestor. */
.kpi-strip .kpi-card--clickable {
    position: relative;
}
.kpi-strip .kpi-card {
    container-type: normal;
    background: transparent;
    box-shadow: none;
    border: none;
    border-radius: 0;
    padding: 0;
    min-width: 0;
    cursor: inherit;
}
.kpi-strip .kpi-card:hover {
    transform: none;
    box-shadow: none;
    border: none;
}
.kpi-strip .kpi-card--clickable:hover {
    cursor: pointer;
}
.kpi-strip .kpi-card--clickable:hover .kpi-value {
    color: var(--primary);
}
.kpi-strip .kpi-icon { display: none; }
.kpi-strip .kpi-content { padding-right: 0; }
/* Typography unified with .stat-chip-label / .stat-chip-value so
   the chip strip on Reports / Dashboard reads as the same primitive
   the admin Dashboard uses. */
.kpi-strip .kpi-label {
    font-size: var(--text-xs);
    margin-bottom: 0;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 500;
    color: var(--text-muted);
}
.kpi-strip .kpi-value {
    font-family: var(--font-display);
    font-size: var(--step-2);
    font-weight: 600;
    line-height: 1.1;
    color: var(--heading);
    letter-spacing: -0.01em;
}
.kpi-strip .kpi-sub {
    font-size: var(--text-xs);
    margin-top: 0.15rem;
    line-height: 1.2;
}
/* Customize-mode remove button: small + tucked into the chip's
   top-right so it doesn't crowd neighboring cells. */
.kpi-strip .kpi-card > button[x-show="showPicker"] {
    position: absolute;
    top: -4px;
    right: -6px;
    width: 16px;
    height: 16px;
    font-size: 0.6rem;
}
.kpi-strip .kpi-card[style*="position:relative"] {
    /* preserve the relative positioning Alpine set for the × button */
    overflow: visible;
}
/* Reports: shorthand c1-c5 → shared semantic kpi-icon colors */
.kpi-icon.c1 { background: var(--primary-light); color: var(--primary); }
.kpi-icon.c2 { background: var(--success-light); color: var(--success); }
.kpi-icon.c3 { background: var(--warning-light); color: var(--warning); }
.kpi-icon.c4 { background: var(--danger-light); color: var(--danger); }
.kpi-icon.c5 { background: var(--info-light); color: var(--info); }

/* Sub-section header */
.report-subheader {
    margin: 1.25rem 0 0.75rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--border-light);
    display: flex; align-items: center; gap: 0.5rem;
}
.report-subheader h4 { margin: 0; font-size: var(--text-body); font-weight: 600; color: var(--heading); }
.report-subheader svg { width: 14px; height: 14px; color: var(--text-muted); }

/* Profitability pills - variable width via flex: 1 + min-width 130px. Value
   scales with pill width (container query) so narrow pills (many job types)
   shrink the % and wide pills (few job types) grow it. */
.profitability-row { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.profitability-pill {
    container-type: inline-size;
    background: var(--bg); border-radius: var(--radius); padding: 0.875rem 1.125rem;
    min-width: 130px; flex: 1; text-align: center;
    border: 1px solid transparent;
    transition: border-color var(--transition-base) ease;
}
.profitability-pill:hover { border-color: var(--border); }
.profitability-type { font-size: var(--text-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
.profitability-value { font-size: clamp(1.125rem, 0.95rem + 5cqw, 1.625rem); font-weight: 700; margin: 0.2rem 0; line-height: 1.2; }


/* Period selector */
.period-select {
    padding: 0.375rem 0.75rem; border-radius: var(--radius-sm);
    border: 1px solid var(--border); background: var(--card);
    font-size: var(--text-sm); color: var(--text); cursor: pointer;
    /* Escape the global `select { width:100% }` so the compare-to + period
       selects sit inline (content-width) instead of stacking full-width. */
    width: auto;
}
.period-select:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 2px rgba(105,108,255,0.15); }

/* "NOW" badge for current-state KPIs not affected by period selector */
.kpi-now {
    font-size: 0.55rem; font-weight: 700; letter-spacing: 0.04em;
    color: var(--primary); background: var(--primary-light);
    padding: 0.1rem 0.35rem; border-radius: var(--radius-xs);
    margin-left: 0.35rem; vertical-align: middle;
}

/* ══════════════════════════════════════════
   Third-Party Library Overrides
   ══════════════════════════════════════════ */

/* ── Event Calendar Styling ── */
#calendar-card { overflow: hidden; max-width: calc(100vw - var(--sidebar-width) - 80px); }
.sidebar-collapsed #calendar-card { max-width: calc(100vw - 68px - 80px); }
.ec { font-family: 'Public Sans', sans-serif; --ec-border-color: var(--border); --ec-today-bg-color: rgba(105, 108, 255, 0.06); }
.ec-toolbar { padding: 0.5rem 0 0.75rem; gap: 0.75rem; }
.ec-title { font-size: var(--step-3); font-weight: 600; color: var(--heading); }
.ec-button { font-size: var(--text-sm); padding: 0.4rem 0.85rem; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--card); color: var(--text); font-weight: 500; cursor: pointer; transition: background var(--transition-base), border-color var(--transition-base); }
.ec-button:hover { background: var(--bg); border-color: var(--primary); color: var(--primary); }
.ec-button.ec-active { background: var(--primary); color: var(--white); border-color: var(--primary); }
.ec-button-group .ec-button { border-radius: 0; }
.ec-button-group .ec-button:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.ec-button-group .ec-button:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.ec-header { font-size: var(--text-sm); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--heading); }
.ec-sidebar { font-size: var(--text-sm); color: var(--text-muted); }
/* Event items - left accent border, white text, hover lift */
.ec-event {
    border-radius: var(--radius-sm);
    border-left: 3px solid;
    cursor: pointer;
    font-size: var(--text-sm);
    padding: 3px 8px 3px 7px;
    transition: box-shadow var(--transition-base), transform var(--transition-base);
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    color: var(--white);
    text-shadow: 0 1px 3px rgba(0,0,0,0.35);
}
.ec-event:hover { box-shadow: 0 3px 8px rgba(0,0,0,0.15); transform: translateY(-1px); }
.ec-event-time { font-weight: 700; font-size: var(--text-xs); opacity: 0.9; display: block; line-height: 1.3; color: var(--white); }
.ec-event-title { font-weight: 600; font-size: var(--text-sm); display: block; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--white); }
.ec-event-body { display: flex; flex-direction: column; gap: 1px; }
.ec-now-indicator { border-color: var(--danger); }
.ec-body { overflow: auto; }
/* List view - clean readable rows matching EventCalendar demo */
.ec-list .ec-day-head { font-family: 'Public Sans', sans-serif; font-weight: 700; font-size: var(--step-2); background: var(--card); color: var(--heading); padding: 0.5rem 1.5rem; border-bottom: 1px solid var(--border); }
.ec-list .ec-event { cursor: pointer; transition: background var(--transition-fast); background: transparent; color: var(--text); text-shadow: none; border-left: none; border-radius: 0; box-shadow: none; padding: 0.425rem 1.25rem; font-size: var(--text-body); align-items: stretch; }
.ec-list .ec-event:hover { background: rgba(105, 108, 255, 0.04); transform: none; }
.ec-list .ec-day.ec-today .ec-event:hover { background: rgba(255, 171, 0, 0.10); }
.ec-list .ec-event-tag { border-radius: 2px; width: 4px; margin-right: 0.5rem; }
.ec-list .ec-event-time { color: var(--text); font-weight: 400; font-size: var(--text-body); opacity: 1; }
.ec-list .ec-event-title { font-family: 'Public Sans', sans-serif; color: var(--text); font-weight: 400; font-size: var(--step-2); white-space: normal; }
.ec-list .ec-no-events { color: var(--text-muted); }
.ec-list .ec-day.ec-today { background: rgba(255, 171, 0, 0.06); }
.ec-list .ec-day.ec-today .ec-day-head { background: transparent; }
/* Resource timeline sidebar */
.ec-timeline .ec-sidebar .ec-resource { font-size: var(--text-sm); font-weight: 500; color: var(--heading); }
.ec-timeline .ec-sidebar { min-width: 120px; background: var(--bg); }


[data-theme="dark"] .ec-list .ec-event:hover { background: rgba(105, 108, 255, 0.08); }


[data-theme="dark"] .ec-list .ec-day.ec-today { background: rgba(255, 171, 0, 0.08); }
.ec-line:first-child { border-top-color: var(--border); }
[data-theme="dark"] .ec { --ec-today-bg-color: rgba(105, 108, 255, 0.08); }
[data-theme="dark"] .ec-button { border-color: var(--border); }


[data-theme="dark"] .ec-day { border-color: var(--border); }

/* Day-overflow popover (".ec-popup") shown when you click "+N more" on a
   month-view cell. Event Calendar's default ships a hardcoded white
   panel that was invisible-on-invisible in dark mode (light header text
   on white bg). Theme using tokens so both modes work from one rule;
   @layer vendor handles the cascade so we don't need !important. */
.ec-popup { background: var(--card); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md); padding: 0.25rem; }
.ec-popup .ec-day-head { background: transparent; color: var(--heading); padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-weight: var(--fw-semibold); }
.ec-popup .ec-day-head a[role="button"] { color: var(--text-muted); cursor: pointer; padding: 0 0.35rem; line-height: 1; }
.ec-popup .ec-day-head a[role="button"]:hover { color: var(--text); }
.ec-popup .ec-events { padding: 0.35rem; display: flex; flex-direction: column; gap: 0.25rem; }

/* Phones: the toolbar's view-mode buttons (Team/List/Month/Week/Day/
   Timeline) overflow a ~375-410px viewport and the rightmost ones get
   clipped off-screen and unreachable. Wrap the toolbar so the view
   switcher drops to its own full-width row, make that row horizontally
   scrollable as a fallback, and tighten button padding. */
@media (width <= 640px) {
    .ec-toolbar { flex-wrap: wrap; row-gap: 0.5rem; }
    .ec-toolbar .ec-end { width: 100%; overflow-x: auto; }
    .ec-title { font-size: var(--step-2) !important; }
    .ec-button { padding: 0.35rem 0.6rem !important; }
}
/* Calendar tooltip - rich KPI-style card */
.ec-tooltip { position: fixed; background: var(--card); color: var(--text); padding: 0; border-radius: var(--radius); max-width: 300px; z-index: var(--z-toast); pointer-events: none; box-shadow: var(--shadow-lg); border: 1px solid var(--border); overflow: hidden; }
.ec-tip-header { font-weight: 700; font-size: var(--text-body); color: var(--heading); padding: 0.625rem 0.75rem 0.35rem; border-bottom: 1px solid var(--border); }
.ec-tip-pills { display: flex; gap: 0.3rem; padding: 0.35rem 0.75rem 0.25rem; flex-wrap: wrap; }
.ec-tip-pill { font-size: var(--text-xs); font-weight: 600; color: var(--white); padding: 0.1rem 0.45rem; border-radius: var(--radius-lg); white-space: nowrap; }
.ec-tip-row { font-size: var(--text-sm); color: var(--text); padding: 0.2rem 0.75rem; display: flex; align-items: center; gap: 0.35rem; }
.ec-tip-row svg { color: var(--text-muted); flex-shrink: 0; }
.ec-tip-desc { font-size: var(--text-sm); color: var(--text-muted); padding: 0.35rem 0.75rem 0.625rem; line-height: 1.4; border-top: 1px solid var(--border); margin-top: 0.25rem; }
[data-theme="dark"] .ec-tooltip { border-color: var(--border); box-shadow: 0 8px 24px rgba(0,0,0,0.4); }


/* ══════════════════════════════════════════
   Logo Variants - universal styling for app logos
   ══════════════════════════════════════════ */
/* White logo on colored background (sidebar badge, loading icon, footer, system admin) */
.logo-inverted img, img.logo-inverted { filter: brightness(0) invert(1); }

/* ══════════════════════════════════════════
   Utility Classes
   ══════════════════════════════════════════ */
.flex { display: flex; }

.flex-between { display: flex; justify-content: space-between; align-items: center; }
.flex-col { display: flex; flex-direction: column; align-items: flex-start; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; min-width: 0; }
.gap-xs { gap: 0.375rem; }
.gap-sm { gap: 0.5rem; }
.gap-md { gap: 0.75rem; }
.gap-lg { gap: 1rem; }
.gap-xl { gap: 1.5rem; }

/* Spacing */
.mb-0 { margin-bottom: 0; }
.mb-xs { margin-bottom: 0.375rem; }
.mb-sm { margin-bottom: 0.5rem; }
.mb-md { margin-bottom: 0.75rem; }
.mb-lg { margin-bottom: 1rem; }
.mb-xl { margin-bottom: 1.5rem; }
.mt-0 { margin-top: 0; }
.mt-xs { margin-top: 0.25rem; }
.mt-sm { margin-top: 0.5rem; }
.mt-md { margin-top: 0.75rem; }
.mt-lg { margin-top: 1rem; }
.mt-xl { margin-top: 1.5rem; }
.mr-xs { margin-right: 0.35rem; }

.ml-xs { margin-left: 0.35rem; }
.ml-sm { margin-left: 0.5rem; }
.p-0 { padding: 0; }

/* Typography size utilities.
   NOTE: historical naming kept for backwards-compat - .text-sm maps to
   --text-base and .text-base maps to --text-md. New views should prefer the
   var-aligned names (.text-body / .text-md / .text-lg) below. */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-body { font-size: var(--text-body); }
.text-lg { font-size: var(--step-2); }
.text-xl { font-size: var(--step-3); }
.text-3xl { font-size: var(--step-4); }
.text-muted { color: var(--text-muted); }
.text-heading { color: var(--heading); }
.text-primary { color: var(--primary); }
.text-success { color: var(--success); }
.text-warning { color: var(--warning); }
.text-danger { color: var(--danger); }
.text-info { color: var(--info); }
/* 0.75rem - fills the gap between .text-xs (0.7) and .text-sm (0.8125) */


.fw-400 { font-weight: var(--fw-normal); font-variation-settings: 'wght' var(--fw-normal); }
.fw-500 { font-weight: var(--fw-medium); font-variation-settings: 'wght' var(--fw-medium); }
.text-bold, .fw-600 { font-weight: var(--fw-semibold); font-variation-settings: 'wght' var(--fw-semibold); }
.fw-700 { font-weight: var(--fw-bold); font-variation-settings: 'wght' var(--fw-bold); }
/* line-through + faded for completed checklist items */
.text-strike { text-decoration: line-through; opacity: 0.6; }
.items-baseline { align-items: baseline; }
.justify-between { justify-content: space-between; }
.border-top { border-top: 1px solid var(--border); }
.pt-lg { padding-top: 1rem; }

.text-right { text-align: right; }
.text-center { text-align: center; }

/* Width-bounded ellipsis truncation for table cells. Sizes in rem so the
   cap scales with the user's root font-size - a 16px → 20px zoom widens
   each tier proportionally rather than holding a hard pixel cap that
   shrinks character count under accessibility zoom. Suffix scale matches
   .col-*: -sm ≈150px, -md ≈200px, -lg ≈300px at the default 16px root.
   Replaces the recurring inline style="max-width:NNNpx;overflow:hidden;
   text-overflow:ellipsis;white-space:nowrap;" pattern on <td> cells.
   Always pair with title="..." so hover reveals the truncated content. */
.cell-truncate-md,
.cell-truncate-lg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cell-truncate-md { max-width: 12.5rem; }
.cell-truncate-lg { max-width: 18.75rem; }

/* Money-delta colorization for ChangeOrders, totals, etc. - replaces inline
   style="color:var(--green,#16a34a);" patterns that hand-rolled their own
   color logic. The semantic colors (success / danger) come from the same
   tokens the rest of the app uses, so dark-theme contrast stays consistent. */
.v-middle { vertical-align: middle; }
.label-caps {
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
    font-weight: var(--fw-semibold);
    font-variation-settings: 'wght' var(--fw-semibold);
}

/* Inline status-change popover used on list views (Jobs, Leads, Estimates,
   Invoices). Replaces an inline compound that referenced 3 undefined CSS
   custom properties with hardcoded light-mode fallbacks, which broke dark
   mode. */
.status-popover {
    position: relative;
    display: inline-block;
}
.status-popover-menu {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: var(--z-popover);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-md);
    padding: 0.25rem 0;
    min-width: 140px;
    margin-top: 0.25rem;
}
.status-popover-menu a {
    display: block;
    padding: 0.35rem 0.75rem;
    font-size: var(--text-sm);
    text-decoration: none;
}

/* Inline code / JSON / XML preview block. Extracted from 15 duplicated
   inline style blocks used for audit logs, API samples, webhook payloads. */
.code-block {
    margin-top: 0.5rem;
    font-size: var(--text-sm);
    font-family: monospace;
    background: var(--bg);
    padding: 0.75rem;
    border-radius: var(--radius-sm);
    overflow-x: auto;
    line-height: 1.6;
}

/* In-card subheading - bold emphasis above a form or list segment.
   Extracted from 15 duplicated inline style blocks. */
.subheading {
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--heading);
    margin-bottom: 0.75rem;
}

.nowrap { white-space: nowrap; }
/* Visually hidden but exposed to screen readers (e.g. a name for an icon-only / action column). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.w-full { width: 100%; }
.w-auto { width: auto; }
.m-0 { margin: 0; }
.no-underline { text-decoration: none; }

.justify-center { justify-content: center; }

/* Icon sizing utilities - for inline Lucide icons */
/* Icon size scale. Use these utility classes instead of inline
   `style="width:Xpx;height:Xpx"` - `flex-shrink:0` here ensures icons never
   squeeze inside flex rows (a common bug where icons got deformed on narrow
   containers). */
.icon-xs { width: 10px; height: 10px; vertical-align: middle; flex-shrink: 0; }
.icon-sm { width: 12px; height: 12px; vertical-align: middle; flex-shrink: 0; }
.icon-md { width: 14px; height: 14px; vertical-align: middle; flex-shrink: 0; }
.icon-base { width: 16px; height: 16px; vertical-align: middle; flex-shrink: 0; }
.icon-lg { width: 18px; height: 18px; vertical-align: middle; flex-shrink: 0; }
.icon-xl { width: 22px; height: 22px; vertical-align: middle; flex-shrink: 0; }
.icon-2xl { width: 32px; height: 32px; vertical-align: middle; flex-shrink: 0; }
.icon-xxl { width: 48px; height: 48px; vertical-align: middle; flex-shrink: 0; }

/* Small inline thumbnail (2.5rem ≈ 40px at default root size). List rows +
   popover-picker rows in the Services / Price-Book feature use this;
   reusable wherever a small image sits inline with text. Sized in rem so
   the thumbnail scales with the user's browser font-size / accessibility
   zoom in step with the surrounding text. flex-shrink:0 so it never
   collapses inside a flex parent. */
.thumb-sm { width: 2.5rem; height: 2.5rem; object-fit: cover; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--border); flex-shrink: 0; }

.card-compact { padding: var(--pad-card-dense); }

/* Chip - inline status / context label, smaller than a button, larger than
   a badge. Used for trial-banner text, plan labels, billing statuses, etc.
   Consumers supply the background and color (via a variant class or inline
   style); this just unifies the shape, spacing, and typography. */
.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.65rem;
    font-size: var(--text-sm);
    font-weight: 500;
    border-radius: var(--radius-sm);
    line-height: 1.4;
}
.chip-primary { background: var(--primary-light); color: var(--primary); }
.chip-muted   { background: var(--bg); color: var(--text-muted); }

/* ── Chip-toggle - a pill that wraps a hidden checkbox and pure-CSS
   styles itself based on the :checked state. Replaces the legacy
   inline-onchange + inline-style-mutation pattern in Settings work-day
   chips and anywhere else that wants a "tag-style" multi-select. ── */
.chip-toggle {
    cursor: pointer;
    padding: 0.3rem 0.7rem;
    border-radius: 2rem;
    font-size: var(--text-sm);
    font-weight: 500;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    user-select: none;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.chip-toggle:hover { border-color: var(--primary); }
.chip-toggle:has(input:checked) {
    background: var(--primary);
    color: var(--white);
    border-color: var(--primary);
}
.chip-toggle:focus-within {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Auth-page brand badge - the rounded logo square shown above the title on
   Login / Signup / ForgotPassword / ForgotCompanyCode / ResetPassword /
   AcceptInvite. Extracted from 6 identical inline style blocks. */
.auth-brand-badge {
    width: 48px; height: 48px;
    background: var(--primary);
    border-radius: var(--radius);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 0.75rem;
}
.auth-brand-badge img {
    width: 65%;
    height: auto;
    filter: brightness(0) invert(1); /* logo-tight.svg is currentColor; force white on the purple tile */
}
/* Wordmark under the badge on pages where a heading tag would collide with
   document heading styles (legal pages). */
.auth-brand-name {
    font-family: var(--font-display);
    font-size: var(--step-1);
    font-weight: 600;
    color: var(--heading);
    margin: 0;
}

/* ── Label hang-ons ──
   Text attached to a label has a role, and role determines size: one
   consistent step down from the label, everywhere. `.label-hint` is the
   muted qualifier ("(optional)", "(internal)"); `.label-action` is a
   label-attached action link ("Forgot?"). Teaching copy belongs in the
   form-field help= tooltip (.label-help), never in a label suffix. */
.label-hint {
    color: var(--text-muted);
    /* em, not a fixed step: the hint sits one notch below WHATEVER label it
       hangs on (body-size form labels and text-xs micro-labels alike). */
    font-size: 0.9em;
    font-weight: 400;
    letter-spacing: normal;
    text-transform: none;
}
.label-action,
[data-theme="dark"] a.label-action:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    color: var(--primary);
    font-size: 0.9em;
    font-weight: 400;
    text-decoration: none;
}
.label-action:hover { text-decoration: underline; }

/* ── Signup plan picker ──
   Radio-card row on /signup: the plan is chosen where the money is taken,
   so the price is visible before any field is typed. Selected state via
   :has(:checked) - no JS. */
.plan-pick {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
    gap: 0.6rem;
    border: 0;
    padding: 0;
    margin: 0 0 1.25rem;
}
.plan-pick-card {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.75rem 0.9rem;
    cursor: pointer;
    font-weight: 400;
    transition: border-color 0.15s, box-shadow 0.15s;
}
.plan-pick-card:hover { border-color: var(--primary); }
.plan-pick-card:has(:checked) {
    border-color: var(--primary);
    box-shadow: 0 0 0 1px var(--primary);
}
.plan-pick-card input[type="radio"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}
.plan-pick-name { font-weight: 700; color: var(--heading); }
.plan-pick-price {
    font-family: var(--font-display);
    font-size: var(--step-2);
    font-weight: 700;
    color: var(--heading);
}
.plan-pick-price small { font-size: var(--text-sm); font-weight: 400; color: var(--text-muted); }
.plan-pick-meta { font-size: var(--text-xs); color: var(--text-muted); }
.plan-pick-blurb { font-size: var(--text-xs); color: var(--text-muted); margin-top: 0.25rem; }

/* Pre-wrap for multiline text */
.pre-wrap { white-space: pre-wrap; }

/* Section header - title sits on the left with action(s) directly next to it.
   Actions are placed right after the title with a small gap, not pushed to the
   far edge of the container; that keeps related controls visually tied to what
   they act on. For the rare case where you want the old space-between layout
   (title left, actions right-aligned), add the `spread` modifier class.

   Supports either a raw heading (<h2/h3/h4>) or a wrapper <div> as the title
   element, followed by one or more action elements. */
.section-header {
    display: flex;
    align-items: center;
    margin-bottom: 1.25rem;
    gap: 0.75rem;
    flex-wrap: wrap; /* wrap actions onto next line on narrow viewports */
}
.section-header h2, .section-header h3, .section-header h4 {
    margin: 0;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    line-height: 1;
    /* no flex:1 - headings take their natural width so actions can sit beside them */
}
.section-header h2 > svg, .section-header h3 > svg, .section-header h4 > svg,
.section-header h2 > i, .section-header h3 > i, .section-header h4 > i { flex-shrink: 0; width: 22px; height: 22px; }
.section-header > div:not([popover]) { min-width: 0; }
.section-header > div:not(:first-child):not([popover]),
.section-header > :not(:first-child):not(h2):not(h3):not(h4):not([popover]) { flex-shrink: 0; display: flex; align-items: center; gap: 0.5rem; }
.section-header small { display: block; color: var(--text-muted); font-size: var(--text-sm); }
/* Pill triggers sit in natural DOM order in the section header - no reorder
   hack needed. */
.section-header .btn-pill { flex-shrink: 0; }
/* Modifier: push actions to the far right edge (old default behaviour). Apply
   only when you intentionally want a wide gap between title and actions - e.g.,
   a search/filter bar that anchors to the right. */
.section-header.spread { justify-content: space-between; }
.section-header.spread > h2:first-child,
.section-header.spread > h3:first-child,
.section-header.spread > h4:first-child,
.section-header.spread > div:first-child { flex: 1; }
/* Pill-shaped inline action button. Used for section-header actions like
   "New Role" / "From Template" / "Add Field" / form-popover triggers
   (popovertarget="..."). */
.btn-pill {
    display: inline-flex; align-items: center; gap: 0.3rem;
    padding: 0.25rem 0.75rem; font-size: var(--text-sm); font-weight: 600;
    border: 1px solid var(--border); border-radius: var(--radius-pill);
    background: var(--card); color: var(--text); cursor: pointer;
    transition: border-color var(--transition-base), background var(--transition-base), color var(--transition-base);
    text-decoration: none; /* for <a> variants */
    line-height: 1.2;
    margin: 0;
}
.btn-pill:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-light); }
.btn-pill i, .btn-pill svg { width: 14px; height: 14px; flex-shrink: 0; }
/* Plus/minus toggle on popover-trigger pill buttons. CSS swaps the visible icon
   based on the state of the sibling [popover] element: plus when closed, minus
   when the popover is open. Requires the trigger and its popover to be adjacent
   siblings (which is how the popovertarget script emits them). */
.btn-pill .pill-icon-open { display: none; }
.btn-pill:has(+ [popover]:popover-open) .pill-icon-closed { display: none; }
.btn-pill:has(+ [popover]:popover-open) .pill-icon-open { display: inline; }
/* Empty state placeholder for empty lists/tables */
.empty-state { text-align: center; padding: 2rem; color: var(--text); }
.empty-state svg, .empty-state i[data-lucide] { width: 32px; height: 32px; opacity: 0.3; margin-bottom: 0.5rem; }
/* Larger empty-state icon - used for inline empty placeholders */
.empty-icon { opacity: 0.15; margin-bottom: 0.75rem; }

/* Table card - card with no padding, allows table to fill edge-to-edge */
.card-table { padding: 0; overflow-x: auto; }

/* ── Mobile stacked-card table ──
   Opt-in utility: add `.table-stacked-mobile` to a <table> (or its wrapper)
   to have the table collapse into a stack of cards on phones. Each <td>
   MUST carry a data-label="Column Name" so the label shows inline next to
   the value. Cells without data-label (spacer/action columns) are hidden
   in mobile mode since they're usually the ⋮/edit/delete column where the
   action sits above the card's content anyway.

   Why opt-in: 2-column key-value tables, short readonly tables, and tables
   where horizontal scroll is actually the right UX (wide data grids) each
   want a different mobile treatment. Forcing this on every table would
   regress some of them. */
@media (width <= 640px) {
    .table-stacked-mobile,
    .table-stacked-mobile thead,
    .table-stacked-mobile tbody,
    .table-stacked-mobile tr,
    .table-stacked-mobile td { display: block; }
    .table-stacked-mobile thead { display: none; }
    /* Specificity guard: `.table-stacked-mobile tr` (0,1,1) outweighs the
       generic `.hidden` (0,1,0), which would leave quick-view rows visible
       in mobile-stacked tables. Restore display:none for any tr that
       carries .hidden - covers the toggle-row pattern used by Jobs /
       Estimates / Invoices / Customers / etc. */
    .table-stacked-mobile tr.hidden { display: none; }
    .table-stacked-mobile tr {
        border-bottom: 1px solid var(--border);
        padding: 0.75rem 1rem;
    }
    .table-stacked-mobile tr:last-child { border-bottom: none; }
    .table-stacked-mobile td {
        padding: 0.25rem 0;
        border: none;
        font-size: var(--text-body);
        display: flex;
        /* flex-wrap + min-width:0 + overflow-wrap let a long unbroken value
           (email, serial #, address, URL) break and drop below its label
           instead of pushing the stacked card wider than the viewport - the
           "stacked table still overflows on phones" symptom. */
        flex-wrap: wrap;
        justify-content: space-between;
        align-items: baseline;
        gap: 0.25rem 0.75rem;
        min-width: 0;
        overflow-wrap: anywhere;
    }
    .table-stacked-mobile td[data-label]::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--text-muted);
        font-size: var(--text-xs);
        text-transform: uppercase;
        letter-spacing: 0.04em;
        flex-shrink: 0;
    }
    /* Cells without a label (typical: action-button column) sit full-width
       at the bottom of the card rather than squeezing between labeled rows.
       CSS `order` pushes them after labeled cells in the flex flow even
       though they appear first in the DOM (e.g. a leading td-expand cell). */
    .table-stacked-mobile td:not([data-label]) {
        margin-top: 0.5rem;
        justify-content: flex-end;
        order: 99;
    }
    
    /* Empty cells were rendering as ghost rows on phones: a
       data-label::before pseudo (e.g. "Phone:") with no value next to
       it because the column genuinely has no data for that row. Hiding
       td:empty drops the empty pair entirely instead of leaving a half-
       finished label hanging. Cells with whitespace text are not :empty
       and stay visible - only truly content-free <td></td> get hidden. */
    .table-stacked-mobile td:empty { display: none; }
    /* tr's flex ordering depends on the tr itself being a flex container. */
    .table-stacked-mobile tr { display: flex; flex-direction: column; }
    /* Quick-view expansion row - opt OUT of the stacked-card treatment.
       The row hosts a single wide <td colspan> with pre-styled content
       (its own padding + accent border). Default mobile rules push it
       right (flex-end) and add outer card padding, which squashes the
       content. Render it as a plain full-width block instead.

       :not(.hidden) is load-bearing: a COLLAPSED quick-view row carries
       both `quick-view-row` and `hidden`. This rule and the `.hidden`
       guard above have equal specificity (0,2,1); without the exclusion
       this later rule wins and reveals every collapsed expand row as an
       empty card on phones. */
    .table-stacked-mobile tr.quick-view-row:not(.hidden) {
        display: block;
        padding: 0;
        border-bottom: none;
    }
    .table-stacked-mobile tr.quick-view-row > td {
        display: block;
        padding: 0;
        margin: 0;
        order: 0;
    }
    .table-stacked-mobile tr.quick-view-row > td::before { content: none; }
    /* The scroll container is no longer needed when rows stack. */
    .card-table:has(.table-stacked-mobile) { overflow-x: visible; }
}

/* Chart height utilities - a MIN-height floor, not a hard height. Frappe renders
   each chart's SVG at a fixed pixel height (its own `height` option) plus a
   legend below it; pinning the container SHORTER than that clipped the chart and
   bled the legend out the bottom of the card. min-height lets the card adapt up
   to whatever Frappe drew (chart-body padding adds the breathing room), while
   still giving an empty/loading chart a sensible floor. Use svh (not dvh) so the
   floor doesn't thrash as mobile browser chrome shows/hides; the px ceiling caps
   the floor at the design-intended size, the svh middle term eases it down on
   short viewports. */
.chart-xs { min-height: clamp(120px, 22svh, 140px); }
.chart-sm { min-height: clamp(160px, 30svh, 200px); }
.chart-md { min-height: clamp(200px, 35svh, 250px); }
.chart-lg { min-height: clamp(240px, 42svh, 320px); }

/* Auth-page form container - centered card with responsive max-width.
   Replaces repeated inline style="width:100%; max-width: min(420px, ...)"
   across Login / Signup / ForgotPassword / ResetPassword / etc. */
.auth-form-card {
    width: 100%;
    max-width: clamp(320px, 100% - 2rem, 480px);
    margin: auto;
}

/* ── Legal pages (Terms / Privacy); centered readable prose column ── */
.legal-page { width: 100%; max-width: min(760px, calc(100vw - 1rem)); margin: 2rem auto; }
.legal-page h1 { font-size: var(--step-3); margin-bottom: 0.5rem; }
.legal-page h2 { font-size: var(--step-1); margin: 1.5rem 0 0.4rem; color: var(--heading); }
.legal-page p { line-height: 1.65; color: var(--text); }

/* ── Equipment QR scan page (public, no-login landing for a scanned label) ── */
.scan-page { max-width: 520px; margin: 2rem auto; padding: 0 1rem; }
.scan-brand { text-align: center; margin-bottom: 1rem; }
.scan-logo { max-height: 48px; margin-bottom: 0.5rem; }
.scan-business { font-family: var(--font-display); font-weight: 600; font-size: var(--text-lg); color: var(--heading); }
.scan-title { margin: 0 0 0.5rem; }
.scan-specs, .scan-history { margin-top: 1rem; }
.scan-history h3 { margin: 0 0 0.5rem; }
.scan-history ul { list-style: none; padding: 0; margin: 0; }
.scan-history li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.4rem 0; border-bottom: 1px solid var(--border); }
.scan-history li:last-child { border-bottom: none; }
.scan-cta { display: block; text-align: center; margin-top: 1rem; }
.scan-foot { text-align: center; color: var(--text-muted); font-size: var(--text-xs); margin-top: 1rem; }

/* ── Filter-row inputs (list pages: search + dropdown filters) ──
   Wrap-friendly size utilities for the common pattern of a search input
   that flexes to fill the row plus one or more fixed-width filter selects
   that stack underneath on narrow viewports. */
/* width:auto is load-bearing: without it the base `input { width:100% }` rule
   makes flex-basis:auto resolve to 100%, so the search claims the whole row and
   the dropdowns wrap below it. width:auto lets flex-grow fill the leftover space
   inline with the selects instead. */
.filter-search { flex: 1 1 auto; min-width: clamp(160px, 30vw, 220px); width: auto; }
/* width:auto for the same reason as .filter-search above - the base
   select { width:100% } would otherwise make flex-basis:auto resolve to 100%
   and stack each dropdown on its own row. min-width keeps them readable; they
   sit at content width inline (no flex-grow), letting the search fill the rest. */
.filter-select-sm { min-width: clamp(100px, 18vw, 140px); width: auto; }
.filter-select-md { min-width: clamp(120px, 22vw, 180px); width: auto; }


/* ── Grid templates for uniform auto-fill behaviour ──
   Replaces per-feature `repeat(auto-fill, minmax(Npx, 1fr))` inline styles.
   All tracks use the defensive min(100%, N) so a single item never overflows
   a narrow container. */
.grid-auto-fill-xs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr)); gap: 0.75rem; }
.grid-auto-fill-sm { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); gap: 1rem; }
.grid-auto-fill-md { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); gap: 1rem; }


/* Hide a column only in the tablet range. Below
   640px the table flips to stacked-card mode (.table-stacked-mobile);
   we don't want this rule fighting that. Modern range syntax
   gives us disjoint viewport bands so no !important needed. */
@media (640px < width <= 900px) {
    .cell-hide-tablet { display: none; }
}

/* ── Table column widths ──
   Replaces per-column `<th style="width: Xpx">` for common action / checkbox
   columns. Widths are fixed because action buttons are icon-sized regardless
   of viewport; data columns flex to fill. */
th.col-checkbox, td.col-checkbox { width: 40px; }

th.col-actions-pair, td.col-actions-pair { width: 90px; }

/* Leading-column row actions - single 32px round kebab in a 48px-wide cell.
   Tighter padding so the kebab sits flush-left in the column, the rest of
   the cell width acts as a clickable strip alongside the row body for the
   quick-view expand. Headers in this column stay empty (no label). */
th.td-actions-leading, td.td-actions-leading {
    width: 48px;
    padding: 0.4rem 0.5rem;
    text-align: center;
    vertical-align: middle;
}
/* Mobile stacked-card mode - pin the kebab to the top-right corner of each
   stacked card instead of letting it flow to the bottom (where actions used
   to live). The wrapper row is .table-stacked-mobile tr (display:flex column),
   absolute-positioning the cell relative to the row itself anchors it to the
   card top-right corner. */
@media (width <= 640px) {
    .table-stacked-mobile tr { position: relative; }
    /* Pin to the top-right corner ONLY when the cell holds a kebab
       (.actions-menu). A wider action here (e.g. a "+ Add" button) is left to
       flow full-width at the card bottom via the generic td:not([data-label])
       rule above, so it never overlaps the labelled rows. */
    .table-stacked-mobile td.td-actions-leading:has(.actions-menu) {
        position: absolute;
        top: 0.4rem;
        right: 0.4rem;
        width: auto;
        padding: 0;
        margin: 0;
        order: -1;
        z-index: 1;
    }
    /* Hide the leading column's empty header on mobile (no label needed) */
    .table-stacked-mobile td.td-actions-leading::before { content: none; }
    /* The kebab is position:absolute at top-right, so the first data
       cell's content was sliding under it on narrow viewports. :has()
       scopes the rule to rows that actually carry a kebab, then pads
       every data-label cell in that row by enough to clear the 32px
       button + 0.4rem corner offset. Padding all data cells (not just
       the topmost) keeps stacked-card alignment uniform without
       needing a "first-of-attribute" pseudo CSS doesn't have. */
    .table-stacked-mobile tr:has(td.td-actions-leading .actions-menu) td[data-label] {
        padding-right: 3rem;
    }
}

/* ── Row tints ──
   Subtle background tint applied to a <tr> to signal status without
   drowning out the row. Derived from theme tokens via color-mix so
   dark mode inverts correctly (dark-mode --danger etc. are different
   hues). 6% alpha is the sweet spot on both themes - 4% washes out in
   dark mode, 8%+ starts competing with the row hover state. */
.row-tint-danger  { background: color-mix(in srgb, var(--danger)  6%, transparent); }
.row-tint-warning { background: color-mix(in srgb, var(--warning) 6%, transparent); }


/* Common link style - no decoration, heading color */
.link-heading { text-decoration: none; color: var(--heading); }
.link-heading:hover { color: var(--primary); text-decoration: underline; }

/* Small metadata sub-text (timestamps, descriptions below headings) */
.meta-sub { margin: 0.15rem 0 0; font-size: var(--text-sm); color: var(--text-muted); }

/* ── Recurring content patterns (extracted from repeated inline styles) ──
   Named per concept so callers compose meaning, not a soup of micro-utils. */
.list-nested { list-style: none; padding: 0.25rem 0 0 1.5rem; margin: 0; }  /* indented borderless sub-list */
.field-label { display: block; margin-bottom: 0.25rem; }                    /* bold block label above a value */
.disclosure-summary { cursor: pointer; padding: 0.4rem 0; }                 /* clickable <details> summary row */
.text-prewrap { white-space: pre-wrap; word-break: break-word; }            /* preserve newlines, wrap long tokens */
.note-body { white-space: pre-wrap; word-break: break-word; margin: 0.25rem 0 0; font-family: inherit; font-size: var(--text-sm); }


/* ── Grid Utilities (mobile-first: single column, expand at breakpoints) ── */
.grid           { display: grid; }
.grid-2 { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@media (width >= 640px) {
    .grid-2     { grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); }
}

/* ── Padding Utilities ── */
.p-sm { padding: 0.75rem; }
.p-md { padding: 1rem; }
.p-lg { padding: 1.25rem; }
.p-xl { padding: 1.5rem; }

/* ── Status Text Indicators ── */
.status-active   { color: var(--success); font-weight: 500; }
.status-inactive { color: var(--danger); font-weight: 500; }

/* ── Display Helpers ── */
.inline          { display: inline; }
.inline-flex     { display: inline-flex; }
.cursor-pointer  { cursor: pointer; }

.list-none       { list-style: none; padding: 0; }

/* Justify-content + text-align utilities to round out the layout primitives. */
.justify-end     { justify-content: flex-end; }


/* Vertical-align nudge for inline Lucide icons placed mid-text - the SVG's
   intrinsic baseline sits slightly above the cap line. Use on `<i>` siblings
   of text inside a flex-aligned heading or inline label. */
.icon-baseline   { vertical-align: -2px; }
.icon-middle     { vertical-align: middle; }

/* Banner-style info row inside a popover / form section: muted bg, padded,
   rounded corners. Replaces the recurring inline
   `style="padding:0.5rem;background:var(--bg);border-radius:var(--radius-sm);"`. */
.popover-info-banner {
    padding: 0.6rem 0.75rem;
    background: var(--bg);
    border-radius: var(--radius-sm);
}

/* ── Service image preview ─────────────────────────────────────────────
   Used in Features/Services/Views/_Form.cshtml for the inline image
   preview / empty / upload-zone column. Three states share the 120x120
   square; modifier classes flip the bg + border. */
.service-image-preview {
    width: 120px;
    height: 120px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--bg);
    border: 1px solid var(--border);
}
.service-image-preview-fallback,
.service-image-preview-empty {
    width: 120px;
    height: 120px;
    background: var(--bg);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    text-align: center;
    padding: 0.5rem;
    box-sizing: border-box;
}
.service-image-preview-fallback { display: none; }
.service-image-upload-col { flex: 1; min-width: 200px; }

/* ── Role matrix table ──────────────────────────────────────────────────
   Settings → Roles permission editor. Sticky thead so the column
   labels stay visible while the table body scrolls. */
.role-matrix-shell {
    max-height: 400px;
    overflow-y: auto;
    /* overflow-x too: the V/C/E/D matrix is wider than a phone-width popover,
       so let it scroll sideways inside the shell instead of overflowing. */
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}
.role-matrix-shell thead {
    position: sticky;
    top: 0;
    background: var(--card);
    z-index: 1;
}
.role-matrix-shell th { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
.role-matrix-shell th.col-vced { width: 48px; text-align: center; padding: 0.5rem 0.3rem; }
.role-matrix-shell td { padding: 0.35rem 0.6rem; border-bottom: 1px solid var(--border); }
.role-matrix-shell td.cell-vced { text-align: center; padding: 0.35rem 0.3rem; }
.role-matrix-shell .group-row td {
    padding: 0.5rem 0.6rem;
    background: var(--bg);
}
.role-matrix-shell .group-suffix {
    float: right;
    text-transform: none;
    letter-spacing: normal;
}
.role-matrix-toolbar {
    margin: 0.75rem 0 0.5rem;
    padding: 0.6rem 0.75rem;
    background: var(--bg);
    border-radius: var(--radius-sm);
}
.role-matrix-search-shell { flex: 1; min-width: 180px; gap: 0.4rem; }
.role-matrix-search-input {
    border: none;
    background: transparent;
    padding: 0.2rem 0;
    width: 100%;
}
.role-matrix-preset-suffix { text-transform: none; letter-spacing: normal; }
.role-matrix-preset-list { gap: 0.4rem; }
.role-matrix-preset-list > * { flex-direction: column; padding: 0.4rem 0.7rem; text-align: left; }

/* ── Weekly schedule table (Capacity / Team) ────────────────────────────
   Replaces inline `text-align:left` on each `<th>` in
   _TechCapacityForm.cshtml. */
.weekly-schedule-table th { text-align: left; }


/* ── Label help-icon ─────────────────────────────────────────────────────
   The `?` icon the <form-field help="..."> attribute emits inline with the
   label. The help text travels in data-tip + aria-label; hover or keyboard-
   focus reveals it through the shared top-layer tooltip (see .ui-tooltip /
   app.js). Saves the vertical space a block-level `<small>` hint would take.

   Markup contract (rendered by FormFieldTagHelper):
     <button type="button" class="label-help" aria-label="..." data-tip="...">
         <i data-lucide="help-circle"></i>
     </button>
   aria-label carries the help text for assistive tech. */
.label-help {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 0.25rem;
    padding: 0;
    width: 16px;
    height: 16px;
    background: transparent;
    border: none;
    color: var(--text-muted);
    cursor: help;
    position: relative;
    vertical-align: middle;
    line-height: 1;
}
.label-help:hover, .label-help:focus-visible { color: var(--primary); }
.label-help i, .label-help svg { width: 14px; height: 14px; pointer-events: none; }

/* ── Shared top-layer tooltip (#ui-tooltip) ──
   The single tooltip element for the whole app: any element with a data-tip
   attribute (form-field .label-help icons, .kpi-card tips, ...) shows it on
   hover/focus, driven by app.js. Lives in the top layer (popover="manual") so
   no ancestor overflow - popover panels, overflow-scroll card-tables, KPI rows
   - can clip it; app.js sets left/top against the trigger and flips
   above<->below at the viewport edge. */
.ui-tooltip {
    position: fixed;
    margin: 0;
    inset: auto;
    border: none;
    overflow: visible;            /* don't clip the ::after arrow */
    background: var(--sidebar);
    color: var(--sidebar-brand-text);
    padding: 0.55rem 0.8rem;
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    line-height: 1.45;
    width: max-content;
    max-width: min(320px, calc(100vw - 1rem));
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
    text-align: left;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    white-space: normal;
    pointer-events: none;
    z-index: var(--z-tooltip);
}
/* Arrow: points down when the tooltip is above its trigger (default), up when
   flipped below. --arrow-x (trigger centre, relative to the tooltip) is set by app.js. */
.ui-tooltip::after {
    content: '';
    position: absolute;
    left: var(--arrow-x, 50%);
    transform: translateX(-50%);
    border: 6px solid transparent;
    top: 100%;
    border-top-color: var(--sidebar);
}
.ui-tooltip.ui-tooltip--below::after {
    top: auto;
    bottom: 100%;
    border-top-color: transparent;
    border-bottom-color: var(--sidebar);
}

/* Compact input variant for date-pickers + small inline controls inside
   booking / portal forms where the standard input height is too imposing. */
.input-compact {
    font-size: 0.95rem;
    padding: 0.4rem 0.6rem;
}


/* Inline-alert banner with severity tint. Used by Team availability form
   for "You're requesting time off" notice. Currently only the danger
   variant exists; add success/info variants as patterns surface. */
.inline-alert {
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-sm);
}
.inline-alert--danger {
    background: color-mix(in srgb, var(--danger) 10%, transparent);
    color: var(--danger);
}

/* ── Rich Empty State ── */
.empty-state-rich { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }
.empty-state-rich .empty-title { font-size: var(--text-body); font-weight: 500; margin: 0 0 0.25rem; color: var(--text); }
.empty-state-rich .empty-desc { font-size: var(--text-sm); opacity: 0.7; margin: 0 0 1rem; }

/* ══════════════════════════════════════════
   Focus & Accessibility Enhancements
   ══════════════════════════════════════════ */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
[role="tab"]:focus-visible,
.btn-pill:focus-visible,
.kpi-card:focus-visible,
[class*="badge"][href]:focus-visible,
.saved-view-pill:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Reduce motion for prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* Table: no zebra stripes - clean single-border design */
th { position: relative; }

/* ══════════════════════════════════════════
   Section Headers (for reports & detail views)
   ══════════════════════════════════════════ */
.section-icon {
    width: 36px; height: 36px;
    border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
}
.section-icon svg { width: 18px; height: 18px; }
.section-icon.primary { background: var(--primary-light); color: var(--primary); }
.section-icon.success { background: var(--success-light); color: var(--success); }
.section-icon.warning { background: var(--warning-light); color: var(--warning); }
.section-icon.info { background: var(--info-light); color: var(--info); }
.section-icon.danger { background: var(--danger-light); color: var(--danger); }
/* Purple variant - no --purple-light token exists, so derive the tint
   from --purple via color-mix. Used for Team/technician section icons. */
.section-icon.purple  { background: color-mix(in srgb, var(--purple) 12%, transparent); color: var(--purple); }

/* ══════════════════════════════════════════
   Progress Bar Component
   ══════════════════════════════════════════ */
.progress { width: 100%; height: 6px; background: var(--border-light); border-radius: var(--radius-xs); overflow: hidden; }
.progress-fill { height: 100%; border-radius: var(--radius-xs); transition: width var(--transition-slow) ease; }
.progress-fill.primary { background: var(--primary); }
.progress-fill.success { background: var(--success); }
.progress-fill.warning { background: var(--warning); }
.progress-fill.danger { background: var(--danger); }
.progress-fill.info { background: var(--info); }

/* ══════════════════════════════════════════
   Dark Mode
   ══════════════════════════════════════════ */

/* Theme toggle icon visibility */
/* Theme icon swapped via JS in toggleTheme() */

[data-theme="dark"] {
    --bg: #1a1b2e;
    --card: #232440;
    --card-hover: #2e2f50;
    --heading: #f4f5fb;
    --text: #e2e4f0;
    --text-muted: #c0c3dc;
    --border: #3d3f68;
    --border-light: #2c2d4a;
    --shadow: 0 2px 8px rgba(0,0,0,0.3);
    --shadow-hover: 0 4px 16px rgba(0,0,0,0.4);
    --shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
    --sidebar: #161728;
    --sidebar-text: #c4c6e0;
    --sidebar-active-bg: rgba(105,108,255,0.25);
    --sidebar-hover: rgba(255,255,255,0.1);
    --sidebar-brand-text: #ececf5;
    --sidebar-section: #9597b8;
    --input-bg: #1e1f38;
    --input-border: #4c4e74;
    --primary: #818CF8;
    --primary-hover: #9ba3ff;
    --primary-light: rgba(129,140,248,0.15);
    --primary-rgb: 129, 140, 248;
    --success: #66BB6A;
    --success-light: rgba(102,187,106,0.15);
    --warning: #FFAB00;
    --warning-light: rgba(255,171,0,0.15);
    --danger: #EF4444;
    --danger-light: rgba(239, 68, 68, 0.18);
    /* Tonal action variants - brighter for dark-mode contrast. Same hue as
       light mode, shifted higher in luminance so 1px strokes and small
       icons stay visible on dark cards. */
    --danger-strong: #FF4040;
    --danger-strong-hover: #FF6A6A;
    --danger-strong-tint: rgba(255, 64, 64, 0.15);
    --danger-strong-tint-stronger: rgba(255, 64, 64, 0.18);
    --success-strong: #4ADE80;
    --success-strong-hover: #86EFAC;
    --success-strong-tint: rgba(74, 222, 128, 0.15);
    --warning-strong: #FBBF24;
    --warning-strong-hover: #FCD34D;
    --warning-strong-tint: rgba(251, 191, 36, 0.15);
    --info: #03C3EC;
    --info-light: rgba(3,195,236,0.15);

    /* Dark mode badge tokens - softer tints for dark backgrounds */
    --badge-primary-bg: rgba(129,140,248,0.18);
    --badge-primary-fg: #a5b4fc;
    --badge-success-bg: rgba(113,221,55,0.18);
    --badge-success-fg: #86efac;
    --badge-danger-bg: rgba(255,62,29,0.18);
    --badge-danger-fg: #fca5a5;
    --badge-warning-bg: rgba(255,171,0,0.18);
    --badge-warning-fg: #fcd34d;
    --badge-info-bg: rgba(3,195,236,0.18);
    --badge-info-fg: #67e8f9;
    --badge-purple-bg: rgba(139,92,246,0.18);
    --badge-purple-fg: #c4b5fd;
    --badge-muted-bg: rgba(192,195,220,0.15);
    --badge-muted-fg: #c0c3dc;
    --badge-orange-bg: rgba(249,115,22,0.18);
    --badge-orange-fg: #fdba74;
}

html[data-theme="dark"] { background: var(--bg); }
[data-theme="dark"] body { background: var(--bg); color: var(--text); }

/* Tables - dark mode th uses darker card variant */
[data-theme="dark"] table th { background: var(--input-bg); }
[data-theme="dark"] tbody td { border-color: var(--border); }
[data-theme="dark"] table a { color: var(--primary); }
[data-theme="dark"] table a:hover { color: var(--primary-hover); }

/* Inputs - dark mode inherits via --input-bg and --input-border vars */
[data-theme="dark"] select option { background: var(--card); color: var(--heading); }
[data-theme="dark"] input::placeholder,
[data-theme="dark"] textarea::placeholder { color: var(--sidebar-section); }

/* Cards & articles */
[data-theme="dark"] .card,
[data-theme="dark"] article { background: var(--card); border-color: var(--border); }

/* Top bar */
[data-theme="dark"] .top-bar { box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
/* hamburger dark mode removed - sidebar-brand handles toggle */
[data-theme="dark"] .topbar-icon-btn { background: var(--border); border-color: var(--border); }


[data-theme="dark"] .sidebar-brand { border-color: var(--border); }

/* Badges - use dark-mode badge variables for consistent tinting */
[data-theme="dark"] .badge { opacity: 1; }
[data-theme="dark"] .badge-draft, [data-theme="dark"] .badge-inactive { background: var(--input-border); }
[data-theme="dark"] .badge-new, [data-theme="dark"] .badge-sent, [data-theme="dark"] .badge-assigned,
[data-theme="dark"] .badge-pending, [data-theme="dark"] .badge-rescheduled { background: var(--badge-primary-bg); color: var(--badge-primary-fg); }
[data-theme="dark"] .badge-approved, [data-theme="dark"] .badge-active, [data-theme="dark"] .badge-completed,
[data-theme="dark"] .badge-paid, [data-theme="dark"] .badge-accepted { background: var(--badge-success-bg); color: var(--badge-success-fg); }
[data-theme="dark"] .badge-rejected, [data-theme="dark"] .badge-overdue, [data-theme="dark"] .badge-cancelled,
[data-theme="dark"] .badge-suspended, [data-theme="dark"] .badge-noshow, [data-theme="dark"] .badge-escalated,
[data-theme="dark"] .badge-outofservice, [data-theme="dark"] .badge-disqualified,
[data-theme="dark"] .badge-declined, [data-theme="dark"] .badge-collections { background: var(--badge-danger-bg); color: var(--badge-danger-fg); }
[data-theme="dark"] .badge-inprogress, [data-theme="dark"] .badge-renewaldue,
[data-theme="dark"] .badge-partiallyapproved, [data-theme="dark"] .badge-flagged,
[data-theme="dark"] .badge-revisionrequested, [data-theme="dark"] .badge-disputed { background: var(--badge-warning-bg); color: var(--badge-warning-fg); }


/* Modal dialog */
[data-theme="dark"] dialog { box-shadow: 0 16px 48px rgba(0,0,0,0.5); }

/* Timeline */
[data-theme="dark"] .timeline { border-color: var(--border); }
[data-theme="dark"] .timeline-entry { border-color: var(--border); }

/* Toasts */
[data-theme="dark"] .toast-success { background: #1a5c2e; }
[data-theme="dark"] .toast-error { background: #7a1a1a; }
[data-theme="dark"] .toast-warning { background: #5c4200; }
[data-theme="dark"] .toast-info { background: #003d4d; }

/* Buttons */
[data-theme="dark"] button.outline,
[data-theme="dark"] a[role="button"].outline { border-color: var(--input-border); color: var(--heading); }
[data-theme="dark"] button.outline:hover,
[data-theme="dark"] a[role="button"].outline:hover { border-color: var(--primary); color: var(--primary); background: rgba(105,108,255,0.08); }
[data-theme="dark"] button.secondary,
[data-theme="dark"] a[role="button"].secondary { background: var(--border); color: var(--heading); border-color: var(--border); }
[data-theme="dark"] button.ghost,
[data-theme="dark"] a.ghost { color: var(--heading); }
[data-theme="dark"] button.ghost:hover,
[data-theme="dark"] a.ghost:hover { background: rgba(255,255,255,0.08); color: var(--white); }

/* Primary filled buttons - ensure white text.
   :not([class*="leaflet-"]) (and matching exclusion in the light-mode
   counterpart at line 610) keeps Leaflet's <a role="button"> zoom
   controls from picking up primary-button visual treatment. @layer
   handles vendor cascade; this exclusion handles app-vs-app cascade
   where this broad selector would otherwise outrank .leaflet-bar a. */
[data-theme="dark"] [role="button"]:where(:not(:is(.secondary, .outline, .ghost, .btn-icon, [class*="leaflet-"]))) {
    color: var(--white);
}

/* Links in body */
[data-theme="dark"] a:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) { color: var(--primary); }
[data-theme="dark"] a:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item):hover { color: var(--primary-hover); }

/* Delete buttons - use softer red for dark mode readability */
[data-theme="dark"] .btn-delete,
[data-theme="dark"] .btn-icon.danger { color: var(--danger); border-color: var(--danger); }
[data-theme="dark"] .btn-delete:hover,
[data-theme="dark"] .btn-icon.danger:hover { background: var(--danger); }

/* Form section headers */
[data-theme="dark"] form h4, [data-theme="dark"] form h5 { border-color: var(--border); }

/* Form actions footer (modal header rule removed along with the bar itself) */
[data-theme="dark"] .form-actions { border-color: var(--border); }

/* Pagination */
[data-theme="dark"] nav[aria-label="Pagination"] a { color: var(--primary); }
[data-theme="dark"] nav[aria-label="Pagination"] a.secondary { color: var(--text-muted); }


/* ── Frappe Charts: dark-mode text + grid-line overrides ──
   Frappe Charts renders axis labels, legend text, and grid lines directly in
   SVG. The library inlines dim hex fills that vanish against our dark bg.
   SVG text is painted via the `fill` attribute (not `color`); grid lines via
   `stroke`. `!important` is required because Frappe sets these inline.

   The broad `.chart-container svg text` selector catches every label the
   library produces (axis ticks, y-values, x-values, data labels) regardless
   of which class Frappe chose to put on them - more reliable than listing
   specific class names, which have shifted between library versions. */
[data-theme="dark"] .chart-container svg text {
    fill: var(--text) !important;
}
/* Legend text is already a lighter class by intent - keep it as body text. */
[data-theme="dark"] .chart-container svg .legend-dataset-text {
    fill: var(--text) !important;
}
/* Data values painted on top of bars/points should really pop. */
[data-theme="dark"] .chart-container svg .chart-label,
[data-theme="dark"] .chart-container svg .data-point-value {
    fill: var(--heading) !important;
}
/* Grid lines, axis lines, dashed separators - use --border so they're
   present but not visually loud. */
[data-theme="dark"] .chart-container svg line,
[data-theme="dark"] .chart-container svg .y-axis-lines,
[data-theme="dark"] .chart-container svg .x-axis-lines,
[data-theme="dark"] .chart-container svg line.dashed {
    stroke: var(--border) !important;
}
/* Chart tooltip (`.graph-svg-tip`) - make sure its background matches the
   dark theme so dark-on-dark text is readable. */
[data-theme="dark"] .chart-container .graph-svg-tip {
    background: var(--sidebar) !important;
    color: var(--heading) !important;
    border: 1px solid var(--border) !important;
}
[data-theme="dark"] .chart-container .graph-svg-tip .title,
[data-theme="dark"] .chart-container .graph-svg-tip .data-point-list,
[data-theme="dark"] .chart-container .graph-svg-tip li {
    color: var(--heading) !important;
}

/* Feed */
[data-theme="dark"] .feed-header { border-color: var(--border); }
[data-theme="dark"] .feed-body { color: var(--text); }

/* Pipeline cards */
[data-theme="dark"] .pipeline-card { border-color: var(--border); }
[data-theme="dark"] .pipeline-card:hover { background: var(--card-hover); }


[data-theme="dark"] .team-header { border-color: var(--border); }
[data-theme="dark"] .dispatch-job { background: var(--input-bg); }
[data-theme="dark"] .dispatch-job:hover { background: var(--card-hover); }


[data-theme="dark"] .map-sidebar-header { border-color: var(--border); }
[data-theme="dark"] .map-job-item { border-color: var(--border); }
[data-theme="dark"] .map-job-item:hover { background: var(--card-hover); }


/* Progress bars */
[data-theme="dark"] .progress { background: var(--border); }


/* Tabs */
[data-theme="dark"] .tabs a { color: var(--text-muted); border-color: transparent; }
[data-theme="dark"] .tabs a.active { border-color: var(--primary); }
[data-theme="dark"] .tabs a:hover { color: var(--heading); }

/* Breadcrumbs */
[data-theme="dark"] .breadcrumb a { color: var(--text-muted); }
[data-theme="dark"] .breadcrumb a:hover { color: var(--primary); }


/* (No dark-mode override needed: .btn-delete / .btn-icon.danger read from
   --danger-strong and --danger-strong-tint, which are redefined in the dark
   [data-theme="dark"] root block with brighter red values. The base rules at
   the top of the file automatically adapt.) */


/* Misc overrides */
[data-theme="dark"] hr { border-color: var(--border); }

[data-theme="dark"] .bulk-action-bar { border-color: var(--border); box-shadow: 0 -2px 8px rgba(0,0,0,0.3); }


/* ══════════════════════════════════════════
   Print Styles
   ══════════════════════════════════════════ */
@media print {
    .sidebar, .top-bar, .no-print { display: none !important; }
    .main-content { margin-left: 0 !important; }
    body { background: #fff; color: #333; font-size: 12pt; }
    .card, article { box-shadow: none; border: 1px solid #ccc; break-inside: avoid; }
    a { color: #333; text-decoration: underline; }
    .badge { border: 1px solid #999; background: transparent !important; }
    table { border-collapse: collapse; }
    th, td { border: 1px solid #ccc; padding: 4pt 8pt; }
}


/* KB search-result snippet (inline under the title in the unified list).
   FTS5's snippet() function emits <mark> tags around hit terms - we style
   them as a soft highlight that reads as "this is why this row matched"
   without competing with the title or the source badge. The snippet body
   gets a one-line clamp so very long matches don't blow up row height. */
.kb-search-snippet {
    line-height: 1.5;
    max-height: 3em;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
}

.kb-search-snippet mark {
    background-color: color-mix(in srgb, var(--warning) 25%, transparent);
    color: inherit;
    padding: 0 0.1em;
    border-radius: 2px;
}

[data-theme="dark"] .kb-search-snippet mark {
    background-color: color-mix(in srgb, var(--warning) 35%, transparent);
}

/* ── .article; shared long-form prose surface ──
   One class for every reading surface (KB articles, Help wiki, anywhere
   else an authored body of paragraphs + headings is shown). Pre-fix
   this lived twice; `.kb-content` here and `.help-content` over in
   help-wiki.css; and the two scales had drifted apart (KB body at
   --text-lg, Help body at --text-md, headings demoted one rung). The
   single source of truth lives here; Help adds zero prose overrides.
   Callout banners inside article markup render as the unified .callout
   component (KBContentRenderer maps the :::note/tip/info/warning/caution
   containers to callout/callout--*). Other surface-specific widgets keep
   their own names (.help-screenshot / .help-mockup / .help-faq /
   .help-process-tiles) since those are baked into authored markdown. */
.article {
    /* Cap at ~72ch for comfortable line length on wide monitors. The
       container can still grow wider for tables and code blocks via the
       child-element overrides below (table { width: 100% }, pre overflow).
       Centered within its card so the 72ch cap doesn't leave a ragged
       right edge on widescreens - the readable column sits in the middle
       of the available card width. */
    max-width: 72ch;
    margin-inline: auto;
    /* Body sits at --text-base (13→14 px); same token every dense UI
       surface uses (table rows, detail pages, lists). Compact-doc
       density rather than long-form reading prose. Headings drop one
       rung from the canonical scale to stay proportionate. */
    font-size: var(--text-sm);
    line-height: var(--lh-body);
    color: var(--text);
    /* Weight inherits from <body> (--fw-body). */
}
/* Heading rhythm - extra space ABOVE so each section breathes apart
   from the previous block, tighter space BELOW so the heading anchors
   to its first paragraph. Sizes one step under the global h1-h4 scale
   so they read as a compact-doc cascade against the 13-14 px body. */
.article h1 { font-size: var(--step-3); margin: 2rem 0 0.5rem; line-height: var(--lh-heading); }
.article h2 { font-size: var(--step-2); margin: 1.75rem 0 0.5rem; line-height: var(--lh-heading); }
.article h3 { font-size: var(--step-1); margin: 1.5rem 0 0.5rem; line-height: var(--lh-subhead); }
.article h4 { font-size: var(--step-0); margin: 1.25rem 0 0.5rem; line-height: var(--lh-subhead); }
.article > :first-child { margin-top: 0; }
.article p { margin: 0 0 1rem; }
.article ul, .article ol { margin: 0 0 1rem; padding-left: 1.5rem; }
.article li { margin: 0 0 0.35rem; }
.article blockquote {
    border-left: 3px solid var(--primary);
    padding: 0.65rem 1.1rem;
    margin: 1rem 0;
    background: var(--primary-light);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    color: var(--text);
}
.article blockquote > :last-child { margin-bottom: 0; }
.article pre {
    background: var(--bg);
    padding: 1rem 1.1rem;
    border-radius: var(--radius-sm);
    overflow-x: auto;
    font-size: var(--text-sm);
    line-height: 1.55;
    margin: 1rem 0;
    border: 1px solid var(--border);
}
.article code {
    background: rgba(105,108,255,0.08);
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-xs);
    font-size: 0.88em;
}
.article pre code { background: none; padding: 0; font-size: inherit; }
.article img {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-sm);
    margin: 1rem 0;
    border: 1px solid var(--border);
}
.article a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
.article a:hover { color: var(--primary-hover); }
.article table { width: 100%; margin: 1rem 0; }
.article hr { margin: 1.5rem 0; border: 0; border-top: 1px solid var(--border); }
/* Strong / em readable contrast inside long-form copy. */
.article strong { font-weight: var(--fw-semibold); color: var(--heading); }
.article em { font-style: italic; }

/* Definition lists - the canonical shape for TDS specs (Voltage / 120V AC etc.)
   and glossary-style entries. <dt> is the term, <dd> the definition. */
.article dl { margin: 1rem 0; }
.article dt {
    font-weight: var(--fw-semibold);
    color: var(--heading);
    margin-top: 0.75rem;
}
.article dt:first-child { margin-top: 0; }
.article dd { margin: 0.15rem 0 0 1.25rem; }

/* Figures - image + caption pair. */
.article figure { margin: 1.25rem 0; }
.article figure img { margin: 0 0 0.4rem; }
.article figcaption {
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-style: italic;
}

/* Collapsible sections - the optional-detail shape for long SOPs. */
.article details {
    margin: 1rem 0;
    padding: 0.75rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg);
}
.article details[open] { background: var(--card); }
.article summary {
    cursor: pointer;
    font-weight: var(--fw-semibold);
    color: var(--heading);
    user-select: none;
}
.article details > :not(summary) { margin-top: 0.75rem; }

/* Keyboard shortcut chip. */
.article kbd {
    display: inline-block;
    padding: 0.1rem 0.45rem;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: 0.85em;
    color: var(--heading);
    background: var(--card);
    border: 1px solid var(--border);
    border-bottom-width: 2px;
    border-radius: var(--radius-xs);
    line-height: 1.2;
}

/* Highlighted text - retains a tinted background even with strict sanitization,
   since <mark> is a semantic primitive (no inline style required). */
.article mark {
    background: rgba(255, 171, 0, 0.22);
    color: inherit;
    padding: 0 0.15rem;
    border-radius: var(--radius-xs);
}

/* KB/Help callouts (rendered from :::note / :::tip / :::info / :::warning /
   :::caution markdown containers) reuse the unified .callout component + tones
   from the <callout> tag helper - one design, not a parallel .kb-callout family.
   In prose there's no icon/body structure (just stacked paragraphs), so flip the
   flex row to block flow and promote a leading "**Title.**" paragraph to a
   headline with the supporting paragraph(s) below. */
.article .callout { display: block; }
.article .callout > :last-child { margin-bottom: 0; }
.article .callout > p:first-child {
    font-family: var(--font-display);
    font-weight: var(--fw-semibold);
    color: var(--heading);
    font-size: var(--text-body);
    margin: 0 0 0.4rem 0;
    line-height: 1.35;
}
.article .callout > p:first-child strong { font-weight: inherit; }

/* ────────────────────────────────────────────────────────────
   KB type badge; the structural classifier (SOP / TDS / ...).
   One pill per article, color-coded by Type. Used on list cards
   AND the detail-page header (same shape; different parent).
   ──────────────────────────────────────────────────────────── */
.kb-type-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-pill, 999px);
    font-size: var(--text-xs);
    font-weight: var(--fw-semibold);
    line-height: 1.4;
    background: var(--primary-light);
    color: var(--primary);
    border: 1px solid color-mix(in srgb, var(--primary) 28%, transparent);
}
.kb-type-badge i { width: 14px; height: 14px; }
.kb-type-sop {
    background: color-mix(in srgb, var(--primary) 14%, transparent);
    color: var(--primary);
    border-color: color-mix(in srgb, var(--primary) 30%, transparent);
}
.kb-type-tds {
    background: color-mix(in srgb, var(--info) 14%, transparent);
    color: var(--info);
    border-color: color-mix(in srgb, var(--info) 30%, transparent);
}
.kb-type-techmanual {
    background: color-mix(in srgb, var(--purple) 14%, transparent);
    color: var(--purple);
    border-color: color-mix(in srgb, var(--purple) 30%, transparent);
}
.kb-type-howto {
    background: color-mix(in srgb, var(--success) 14%, transparent);
    color: var(--success);
    border-color: color-mix(in srgb, var(--success) 30%, transparent);
}
.kb-type-troubleshooting {
    background: color-mix(in srgb, var(--warning) 14%, transparent);
    color: var(--warning);
    border-color: color-mix(in srgb, var(--warning) 30%, transparent);
}
.kb-type-reference {
    background: color-mix(in srgb, var(--cyan) 14%, transparent);
    color: var(--cyan);
    border-color: color-mix(in srgb, var(--cyan) 30%, transparent);
}
.kb-type-announcement {
    background: color-mix(in srgb, var(--orange) 14%, transparent);
    color: var(--orange);
    border-color: color-mix(in srgb, var(--orange) 30%, transparent);
}

/* ────────────────────────────────────────────────────────────
   KB reader two-column layout; sticky TOC + article body.
   At ≥980px the TOC sits to the left of the article and sticks
   below the topbar while you scroll. Below 980px the TOC
   collapses to a <details> accordion above the article.
   ──────────────────────────────────────────────────────────── */
.kb-reader {
    display: grid;
    grid-template-columns: minmax(220px, 240px) minmax(0, 1fr);
    gap: 1.5rem;
    align-items: start;
}
.kb-reader-notoc {
    grid-template-columns: minmax(0, 1fr);
}
.kb-reader-body {
    /* The article card keeps its existing card styling but stops trying
       to center .article with margin-inline:auto so the TOC anchors
       align with their target headings. */
    min-width: 0;
}
.kb-reader-body .article {
    /* Slightly looser cap inside the two-column reader (the sidebar
       already constrains horizontal width). */
    max-width: 78ch;
}
.kb-toc {
    position: sticky;
    top: calc(var(--topbar-height, 62px) + 1rem);
    max-height: calc(100dvh - var(--topbar-height, 62px) - 2rem);
    overflow-y: auto;
}
.kb-toc-mobile { display: none; }
.kb-toc-desktop h6 {
    margin: 0 0 0.5rem;
    color: var(--text-muted);
    font-size: var(--text-xs);
    letter-spacing: 0.05em;
}
.kb-toc-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-left: 2px solid var(--border);
}
.kb-toc-list li { margin: 0; }
.kb-toc-list a {
    display: block;
    padding: 0.3rem 0.8rem;
    margin-left: -2px;
    border-left: 2px solid transparent;
    color: var(--text);
    text-decoration: none;
    font-size: var(--text-sm);
    line-height: 1.4;
    transition: color 0.1s, border-color 0.1s;
}
.kb-toc-list a:hover {
    color: var(--primary);
    border-left-color: var(--primary);
}
.kb-toc-h3 a {
    padding-left: 1.5rem;
    font-size: var(--text-xs);
    color: var(--text-muted);
}
@media (width <= 980px) {
    .kb-reader {
        grid-template-columns: minmax(0, 1fr);
    }
    .kb-toc {
        position: static;
        max-height: none;
        overflow: visible;
    }
    .kb-toc-mobile { display: block; }
    .kb-toc-desktop { display: none; }
    .kb-toc-mobile summary {
        cursor: pointer;
        padding: 0.5rem 0.75rem;
        background: var(--card);
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        display: inline-flex;
        align-items: center;
        gap: 0.4rem;
        font-weight: var(--fw-semibold);
        color: var(--heading);
    }
    .kb-toc-mobile[open] summary { margin-bottom: 0.5rem; }
}

/* ────────────────────────────────────────────────────────────
   Print stylesheet; strip chrome, render black-on-white for
   readable PDF export. Same approach as the Estimate / Invoice
   print views but scoped to the article reader.
   ──────────────────────────────────────────────────────────── */
@media print {
    /* Chrome that shouldn't appear on paper. */
    .sidebar,
    .top-bar,
    .kb-toc,
    .actions-menu,
    .actions-panel,
    .breadcrumb,
    .page-header .flex .actions-menu,
    accordion-section,
    .toast-container {
        display: none !important;
    }
    body, .main-content, .main-body, #page-content, #content-area {
        background: #fff !important;
        color: #000 !important;
    }
    .kb-reader {
        display: block;
    }
    .kb-reader-body,
    .card {
        background: #fff !important;
        box-shadow: none !important;
        border: 0 !important;
        padding: 0 !important;
    }
    .article {
        max-width: 100% !important;
        font-size: 11pt !important;
        line-height: 1.5 !important;
        color: #000 !important;
    }
    .article h1, .article h2, .article h3, .article h4 {
        color: #000 !important;
        page-break-after: avoid;
    }
    .article h2 { font-size: 14pt !important; margin-top: 1.2em; }
    .article h3 { font-size: 12pt !important; }
    .article a {
        color: #000 !important;
        text-decoration: underline;
    }
    .article pre,
    .article blockquote,
    .article figure,
    .article table {
        page-break-inside: avoid;
    }
    .article img { max-width: 100% !important; }
    .article .callout {
        background: transparent !important;
        border: 1px solid #999 !important;
        border-left: 4px solid #000 !important;
        color: #000 !important;
    }
    /* Force-show URLs after links so printed PDFs are actionable. */
    .article a[href^="http"]::after {
        content: " (" attr(href) ")";
        font-size: 0.85em;
        color: #444;
    }
}

/* ────────────────────────────────────────────────────────────
   Industry chip-grid; multi-checkbox shape for the Master KB
   Library article form. Each chip is a label-wrapped checkbox
   that toggles a tinted background when checked. Pattern reused
   anywhere a multi-select-of-fixed-options is the right input.
   ──────────────────────────────────────────────────────────── */
/* ── <multi-select> Tag Helper ──
   Closed-by-default dropdown built on a native <details> element.
   Browser owns open/close (click the summary; ESC when focused on it).
   The panel is absolutely positioned INSIDE the <details>, so it
   anchors to its container naturally; no popover, no JS positioning.
   The "multi-select companion" block in wwwroot/js/app.js handles
   click-outside dismiss + keeps
   the summary label in sync as checkboxes toggle. */
.multi-select {
    position: relative;
    width: 100%;
}
.multi-select-host { width: 100%; }
.multi-select-trigger {
    /* list-style:none removes the default disclosure triangle on
       Chrome/Firefox; Safari uses ::-webkit-details-marker. */
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
    padding: 0.3rem 0.65rem;
    font-family: inherit;
    font-size: var(--text-sm);
    color: var(--heading);
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    line-height: 1.5;
    transition: border-color var(--transition-base) ease, box-shadow var(--transition-base) ease;
}
.multi-select-trigger::-webkit-details-marker { display: none; }
.multi-select-trigger::marker { content: ""; }
.multi-select-trigger:hover { border-color: var(--primary); }
details[data-multi-select][open] > .multi-select-trigger {
    border-color: var(--primary);
    box-shadow: var(--shadow-focus-ring);
}
.multi-select-summary {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.multi-select-chevron {
    flex: 0 0 auto;
    transition: transform 0.15s ease;
    color: var(--text-muted);
}
details[data-multi-select][open] .multi-select-chevron {
    transform: rotate(180deg);
}
.multi-select-panel {
    /* Anchor to the .multi-select (position:relative) box. */
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: 50;
    padding: 0.5rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-md, 0 8px 24px rgba(0,0,0,0.2));
}
.multi-select-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr));
    gap: 0.15rem 0.4rem;
    max-height: min(50dvh, 360px);
    overflow-y: auto;
}
.multi-select-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.4rem;
    border-radius: var(--radius-xs);
    cursor: pointer;
    font-size: var(--text-sm);
    user-select: none;
}
.multi-select-row:hover { background: var(--bg); }
.multi-select-row input[type="checkbox"] {
    margin: 0;
    accent-color: var(--primary);
    flex: 0 0 auto;
}
.multi-select-label {
    display: block;
}

/* ── KB article editor (textarea + toolbar) ──
   Replaces Pell. The editor is a plain <textarea>; toolbar buttons
   wrap the selection (bold/italic/code) or prefix the current line
   (heading/list/quote) with the matching Markdown syntax. No live
   preview pane; the rendered article is on the detail page after
   save. See wwwroot/js/md-editor.js for the JS shim. */
.md-editor {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--card);
}
.md-editor-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem;
    padding: 0.35rem 0.5rem;
    border-bottom: 1px solid var(--border);
    background: transparent;
}
/* The md-editor toolbar exists inside .md-editor-toolbar so the global
   button :not() exclusion chain (see ~line 583) adds .md-editor-toolbar
   children to its opt-out list. The rules below paint the toolbar
   buttons to match the rest of the editor chrome (transparent ground,
   muted text, subtle hover). */
.md-editor-toolbar button {
    background: transparent;
    border: 0;
    color: var(--text-muted);
    padding: 0.3rem 0.55rem;
    border-radius: var(--radius-xs);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--fw-semibold);
    cursor: pointer;
    line-height: 1;
}
.md-editor-toolbar button:hover { color: var(--heading); background: var(--bg); }
/* The callout dropdown shares the toolbar's flat chrome. Overrides the
   global `select { width: 100% }` form rule so it sizes to its content
   and sits inline with the buttons. */
.md-editor-toolbar select {
    width: auto;
    background: transparent;
    border: 0;
    color: var(--text-muted);
    padding: 0.3rem 0.45rem;
    border-radius: var(--radius-xs);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--fw-semibold);
    line-height: 1;
    cursor: pointer;
}
.md-editor-toolbar select:hover { color: var(--heading); background: var(--bg); }
.md-editor-toolbar select:focus { box-shadow: none; }
.md-editor-source {
    display: block;
    width: 100%;
    border: 0;
    border-radius: 0;
    background: var(--card);
    color: var(--heading);
    font-family: var(--font-body);
    font-size: var(--text-body);
    line-height: 1.7;
    padding: 1.25rem;
    min-height: 360px;
    resize: vertical;
    outline: none;
}
.md-editor-source:focus { outline: none; box-shadow: none; }

/* ── Quick View (inline row expansion) ── */
.quick-view-row td { background: var(--bg); animation: expandIn 0.2s ease-out; }
/* When the browser supports interpolate-size (Chrome/Edge 129+), upgrade the
   hardcoded max-height:500px trick to a real auto-height fade+grow. The
   content can be any height; no truncation, no overshoot delay. */
@supports (interpolate-size: allow-keywords) {
    :root { interpolate-size: allow-keywords; }
    .quick-view-row td { animation: expandInFluid 0.2s ease-out; }
    @keyframes expandInFluid { from { opacity: 0; block-size: 0; } to { opacity: 1; block-size: auto; } }
}
/* Expandable-row affordance. Every <tr onclick="toggleQuickView(...)">
   gets a subtle chevron appended to its last cell so the click-to-expand
   action reads at a glance. Muted at rest, brightens on hover, rotates
   90deg to point down when the row is expanded. Pure CSS - the trigger
   selector matches the onclick attribute, so no per-list markup is
   needed: every existing expandable row picks the indicator up for free. */
tr[onclick*="toggleQuickView"] > td:last-child::after {
    content: "\203A"; /* › single right-pointing angle quotation */
    display: inline-block;
    margin-left: 0.6rem;
    color: var(--text-muted);
    opacity: 0.4;
    font-size: 1.1em;
    line-height: 1;
    vertical-align: middle;
    transition: opacity var(--transition-base), transform 0.2s ease;
    transform-origin: center;
}
tr[onclick*="toggleQuickView"]:hover > td:last-child::after { opacity: 0.85; }
tr[onclick*="toggleQuickView"].quick-view-active > td:last-child::after {
    opacity: 0.85;
    transform: rotate(90deg);
}
.quick-view-active { background: color-mix(in srgb, var(--primary) 4%, transparent); }

/* Quick-view wrapper - single styled surface for the row-expansion content.
   Hoisted out of each entity's _QuickView.cshtml so the AJAX-fetched body
   and the server-pre-rendered custom-fields block render inside ONE
   container (previously they were two visually-disconnected panels with
   different backgrounds - the user-reported "Custom Fields outside the
   body" symptom). The colored left-border picks up entity context via
   modifier classes (.qv-wrap--info / --success / --primary). */
.qv-wrap {
    padding: 0.75rem 1rem;
    background: var(--bg);
    border-left: 3px solid var(--primary);
    /* Weight inherits from <body> (--fw-body 380) - same baseline as
       the row above it, no per-surface redeclaration. */
}
.qv-wrap--info    { border-left-color: var(--info); }
.qv-wrap--success { border-left-color: var(--success); }
.qv-wrap--primary { border-left-color: var(--primary); }
.qv-wrap--danger  { border-left-color: var(--danger); }
.qv-wrap--warning { border-left-color: var(--warning); }

/* ── Quick-view affordance polish ──
   Hover-highlight on a cursor:pointer row signals "click me" before
   the user reaches for the cell. Chevron rotates when the row is
   expanded (.quick-view-active is added/removed by toggleQuickView)
   to give visual feedback. */
tr.cursor-pointer { transition: background var(--transition-base) ease; }
tr.cursor-pointer:hover { background: color-mix(in srgb, var(--primary) 5%, transparent); }
tr.cursor-pointer [data-lucide="chevron-down"] { transition: transform var(--transition-base) ease; }
tr.cursor-pointer.quick-view-active [data-lucide="chevron-down"] { transform: rotate(180deg); }
/* Each section inside the wrapper (the AJAX body, the custom-fields block)
   gets a subtle separator above it instead of its own background. */
.qv-wrap > * + .qv-section,
.qv-wrap > .qv-section + *,
.qv-wrap .qv-fetch-target + .qv-section {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px dashed var(--border);
}
.qv-section-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    font-weight: 600;
    margin: 0 0 0.35rem;
    display: block;
}

/* ── Enriched quick-view layout primitives ──
   Used by the richer tenant quick-views (Customers / Jobs / Estimates /
   Invoices). Same shape MasterLibrary's quick-view uses, hoisted out so
   each _QuickView partial doesn't redeclare its own inline grids. */
.qv-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.6rem;
    align-items: center;
}
.qv-chip-link {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: var(--text-xs);
    color: var(--text);
    text-decoration: none;
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-sm);
    background: var(--bg);
    transition: background var(--transition-base) ease;
    /* A long email/address chip wraps inside itself instead of overflowing
       the reveal panel on phones. */
    max-width: 100%;
    min-width: 0;
    overflow-wrap: anywhere;
}
.qv-chip-link:hover { background: color-mix(in srgb, var(--primary) 8%, var(--bg)); color: var(--primary); }
.qv-chip-link i { color: var(--text-muted); flex-shrink: 0; }

/* ── Quick-view chips ── one primitive, reused everywhere.
   .qv-fields is a wrap row; .qv-field is a compact vertical mini-card (micro
   uppercase label on top, value below - the KPI-tile shape, smaller). Both the
   <qv-field> and <audit-fields> tag helpers emit .qv-field, and every
   quick-view (simple info, enriched stats, audit footer, custom fields) drops
   them into a .qv-fields row - so the whole reveal reads the same and long
   values wrap inside the pill (min-width:0 + overflow-wrap) instead of
   collapsing into a tall single column on phones. */
.qv-fields {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.5rem;
    align-items: stretch;
}
/* Trailing group (the audit footer) gets a dashed rule above it. */
.qv-fields--footer {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px dashed var(--border);
}
.qv-field {
    display: inline-flex;
    flex-direction: column;
    gap: 0.05rem;
    max-width: 100%;
    min-width: 0;
    padding: 0.3rem 0.6rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow-wrap: anywhere;
}
.qv-field-label {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    line-height: 1.25;
}
.qv-field-value {
    min-width: 0;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--heading);
    line-height: 1.3;
    overflow-wrap: anywhere;
}
.qv-field-value a { color: inherit; }
.qv-field-value .badge { vertical-align: baseline; }
@keyframes expandIn { from { opacity: 0; max-height: 0; } to { opacity: 1; max-height: 500px; } }

/* ── Profile page ── scoped layout classes so Features/Profile/Views/Index.cshtml
   doesn't need inline styles. All sizing/colors pull from theme tokens. */
.profile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
    gap: 1.5rem;
    align-items: start;
}
.profile-card { text-align: center; padding: 2rem 1.5rem; }
.profile-avatar {
    width: 80px; height: 80px;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 1.75rem; font-weight: 700; color: #fff;
    margin: 0 auto 1rem;
    /* background is set inline from Model.CalendarColor - only dynamic bit. */
}
.profile-divider { margin: 1rem 0; }
.profile-meta-label {
    display: block;
    margin-bottom: 0.25rem;
    color: var(--text-muted);
}
.profile-section-title {
    margin: 0 0 1rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* ── MyJobs / Job Detail ── scoped classes that replace what used to
   be inline styles on Features/MyJobs/Views/Detail.cshtml. */
/* Completion-readiness card: 3px left border whose color is set inline
   per state (success when all-done, warning when not). */
.readiness-card { border-left: 3px solid; }
.readiness-list { font-size: var(--text-sm); }
.readiness-item { padding: 0.15rem 0; }
.readiness-icon { flex-shrink: 0; }
.readiness-done { color: var(--text-muted); text-decoration: line-through; }
.readiness-todo { color: var(--heading); }

/* Generic "accent card" that uses --primary as both the background tint
   and the left border. Used for the arrival/departure info block in
   MyJobs detail - could apply anywhere a primary-tinted info card fits. */
.card-accent-primary {
    background: var(--primary-light);
    border-left: 3px solid var(--primary);
}

/* ── My Jobs (technician list) ── the per-job cards on /my-jobs. Built
   to be thumb-friendly in the field: generous tap targets, scannable
   meta grid, full-width action buttons on phones. */
.myjob-list { display: flex; flex-direction: column; gap: 1rem; }
.myjob-card { display: flex; flex-direction: column; gap: 0.9rem; }
.myjob-card--late { border-left: 3px solid var(--danger); }

.myjob-card-head {
    display: flex; justify-content: space-between; align-items: flex-start;
    flex-wrap: wrap; gap: 0.5rem;
}
.myjob-id { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.myjob-num {
    font-family: var(--font-display);
    font-size: var(--step-2);
    font-weight: 600;
    color: var(--heading);
}
.myjob-num:hover { color: var(--primary); }
.myjob-lead { font-size: var(--text-xs); }
.myjob-badges { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }

.myjob-meta {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
    gap: 0.75rem 1.25rem;
}
.myjob-meta-cell { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.myjob-meta-label {
    display: inline-flex; align-items: center; gap: 0.3rem;
    font-size: var(--text-xs); font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--text-muted);
}
.myjob-meta-label i, .myjob-meta-label svg { width: 13px; height: 13px; }
.myjob-meta-value { font-size: var(--text-body); color: var(--heading); font-weight: 500; }
.myjob-meta-sub { font-size: var(--text-sm); color: var(--text-muted); }
a.myjob-meta-sub:hover { color: var(--primary); text-decoration: underline; }

.myjob-detail-toggle { font-size: var(--text-sm); }
.myjob-detail-toggle summary {
    display: inline-flex; align-items: center; gap: 0.35rem;
    cursor: pointer; color: var(--text); font-weight: 500;
}
.myjob-detail-toggle summary i, .myjob-detail-toggle summary svg { width: 14px; height: 14px; }
.myjob-detail-toggle p { margin: 0.4rem 0 0; color: var(--text); }
.myjob-desc { margin: 0; font-size: var(--text-sm); color: var(--text); }

.myjob-actions {
    display: flex; gap: 0.5rem; flex-wrap: wrap;
    padding-top: 0.75rem; border-top: 1px solid var(--border);
}
.myjob-actions form { margin: 0; }

/* Completed jobs - collapsed history list, each row a link to its detail. */
.myjob-completed { margin-top: 1.25rem; }
.myjob-completed > summary {
    display: inline-flex; align-items: center; gap: 0.4rem;
    cursor: pointer; font-weight: 600; color: var(--heading);
}
.myjob-completed-list { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.75rem; }
.myjob-completed-row {
    display: flex; align-items: center; gap: 0.75rem;
    padding: 0.6rem 0.85rem; background: var(--card);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    color: var(--text);
}
.myjob-completed-row:hover { border-color: var(--primary); }
.myjob-completed-num { font-weight: 600; color: var(--heading); flex-shrink: 0; }
.myjob-completed-cust {
    flex: 1; min-width: 0; color: var(--text-muted); font-size: var(--text-sm);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Phones: stack action buttons full-width for big tap targets. */
@media (width <= 560px) {
    .myjob-num { font-size: var(--step-1); }
    .myjob-actions { gap: 0.6rem; }
    .myjob-actions > * { flex: 1 1 100%; }
    .myjob-actions form,
    .myjob-actions form button,
    .myjob-actions > a { width: 100%; }
}

/* Inline callout for short blocks of warning copy (access instructions,
   gotchas, edge notes). Uses the warning theme token so it flips
   correctly in dark mode; was previously hardcoded #fef3c7. */
.callout-warning {
    background: color-mix(in srgb, var(--warning) 15%, transparent);
    padding: 0.75rem;
    border-radius: var(--radius-sm);
}

/* <callout> tag helper - tinted, left-bordered notice banner (icon + body).
   Centralises the hand-rolled "card-compact + inline border-left + flex row
   + icon" banners. Distinct from the legacy single-dash .callout-warning. */
.callout {
    display: flex;
    gap: 0.65rem;
    align-items: flex-start;
    padding: 0.75rem 1rem;
    border-radius: var(--radius-sm);
    border-left: 4px solid var(--border);
    background: var(--card);
    margin-bottom: 1rem;
}
.callout-icon { flex-shrink: 0; width: 18px; height: 18px; margin-top: 0.1rem; }
.callout-title { display: block; margin-bottom: 0.15rem; }
.callout-body { min-width: 0; }
/* Single-line layout for action-banner callouts: bold lead-in + body
   text + optional trailing action button on one row. The text span
   wraps INTERNALLY (its own text flow) when long, while the trailing
   button stays parked on the right - no flex-wrap on the row itself,
   which would push the button to its own line at narrow widths. */
.callout-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
}
.callout-row > :first-child { flex: 1 1 auto; min-width: 0; }
.callout-row > :last-child { flex-shrink: 0; margin-left: auto; }
/* Tighter padding on row-mode callouts so the banner is the visual
   weight of one toolbar row, not a sectioned panel. */
.callout:has(.callout-row) { padding: 0.55rem 0.85rem; align-items: center; }
.callout:has(.callout-row) .callout-icon { margin-top: 0; }
.callout--danger  { border-left-color: var(--danger);  background: color-mix(in srgb, var(--danger) 10%, transparent); }
.callout--danger  .callout-icon { color: var(--danger); }
.callout--warning { border-left-color: var(--warning); background: color-mix(in srgb, var(--warning) 12%, transparent); }
.callout--warning .callout-icon { color: var(--warning); }
.callout--info    { border-left-color: var(--info);    background: color-mix(in srgb, var(--info) 10%, transparent); }
.callout--info    .callout-icon { color: var(--info); }
.callout--success { border-left-color: var(--success); background: color-mix(in srgb, var(--success) 12%, transparent); }
.callout--success .callout-icon { color: var(--success); }

/* Collapsible "learn more" disclosure (<info-note>): the quiet middle ground between
   an info-tip (?) and a callout banner. One muted line collapsed; expands inline and
   stays open. Reserve callouts for operational must-sees; teaching/reference goes here. */
.info-note { border: 1px solid var(--border); border-radius: var(--radius-sm); margin: 0.5rem 0; }
.info-note-summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 0.4rem; padding: 0.4rem 0.65rem; font-size: var(--text-sm); color: var(--text-muted); user-select: none; }
.info-note-summary::-webkit-details-marker { display: none; }
.info-note-summary:hover { color: var(--text); }
.info-note-icon { width: 15px; height: 15px; flex-shrink: 0; }
.info-note-chev { width: 15px; height: 15px; flex-shrink: 0; margin-left: auto; transition: transform 0.12s ease; }
.info-note[open] .info-note-chev { transform: rotate(90deg); }
.info-note-body { padding: 0.1rem 0.75rem 0.6rem 1.7rem; font-size: var(--text-sm); color: var(--text-muted); line-height: 1.55; }

/* Bottom-bordered row used for vertically-stacked notes (tab panels
   rendering a flat list of note bubbles). */
.note-row { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }

/* Signature canvas + saved signature preview image. Hardcoded #fff
   background is correct - the ink is always dark so a white background
   ensures contrast in both themes. */
.signature-canvas {
    border: 2px solid var(--border);
    border-radius: var(--radius-sm);
    background: #fff;
    cursor: crosshair;
    touch-action: none;
    display: block;
    max-width: 100%;
}
.signature-row { padding: 1rem; margin-bottom: 0.75rem; }
.signature-preview {
    max-width: 300px;
    max-height: 100px;
    margin-top: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: #fff;
}

/* Vertical padding utility used by centered empty-state-lite messages
   inside tab panels where a full _EmptyState.cshtml partial would be
   too heavy. */
.py-md { padding-top: 0.75rem; padding-bottom: 0.75rem; }

/* Miscellaneous small utilities - generic, referenced by multiple views. */
.d-block    { display: block; }
.hidden     { display: none; }     /* counterpart to .d-block; pairs with the [x-cloak] rule */
.ml-auto    { margin-left: auto; } /* push self to the far end of a flex row */
.break-word { word-break: break-word; }
th.col-width-100, td.col-width-100 { width: 100px; }
th.col-width-140, td.col-width-140 { width: 140px; }

/* ── QuickBooks Connected page ── scoped classes so Features/QuickBooks/
   Views/_Connected.cshtml renders without inline styles. Every value here
   is theme-token-derived or font-scale-derived; zero hex. */
.qbo-setup-banner {
    border-left: 4px solid var(--warning);
    padding: 1rem 1.25rem;
}
.qbo-stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
    gap: 1rem;
}
.qbo-stat-card    { padding: 1rem 1.25rem; }
.qbo-stat-text    { font-size: var(--text-body); font-weight: 600; margin-bottom: 0.25rem; }
.qbo-actions-card { padding: 1rem 1.25rem; }


/* ── Saved Filter Views ── */
.saved-views-bar { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-bottom: 0.5rem; align-items: center; }
.saved-view-pill {
    display: inline-flex; align-items: center; gap: 0.15rem;
    padding: 0.2rem 0.6rem; border-radius: var(--radius-lg); font-size: var(--text-sm);
    background: var(--primary-light); color: var(--primary);
    text-decoration: none; cursor: pointer; white-space: nowrap;
}
.saved-view-pill:hover { background: var(--primary); color: var(--white); }

/* ── Column Sort Headers ── */
th .sort-link { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 0.25rem; }
th .sort-link:hover { color: var(--primary); }
th .sort-indicator { font-size: var(--text-xs); opacity: 0.7; }

/* Skip to content link (accessibility) */
.skip-link { position: absolute; top: -40px; left: 0; z-index: var(--z-skip); padding: 0.5rem 1rem; background: var(--primary); color: var(--white); text-decoration: none; font-size: var(--text-body); border-radius: 0 0 var(--radius-sm) 0; }
.skip-link:focus { top: 0; }

/* ── Navigation progress bar ──
   Thin top-edge bar for boosted page navigations. Base state is invisible
   with NO transition, so resetting after a nav snaps it back instantly.
   .is-loading creeps width 0->90% over a long ease-out (decelerates, never
   reaches 90% during a normal sub-second swap); .is-done snaps to 100% and
   fades out. JS only adds the bar past a delay threshold, so a fast nav
   never shows it. Token-driven, so dark mode flips for free. */
#nav-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 2px;
    width: 0;
    opacity: 0;
    background: var(--primary);
    box-shadow: 0 0 8px var(--primary);
    border-radius: 0 2px 2px 0;
    z-index: var(--z-toast);
    pointer-events: none;
}
#nav-progress.is-loading {
    opacity: 1;
    width: 90%;
    transition: width 8s cubic-bezier(0.1, 0.9, 0.2, 1), opacity 120ms ease;
}
#nav-progress.is-done {
    opacity: 0;
    width: 100%;
    transition: width 160ms ease, opacity 300ms ease 120ms;
}
@media (prefers-reduced-motion: reduce) {
    #nav-progress { transition: none !important; }
}

/* ══════════════════════════════════════════
   Responsive Breakpoints - all media queries grouped
   ══════════════════════════════════════════
   Breakpoint scale (mobile-first thinking, declared max-width):
     1200px  - large desktop / small laptop
     1024px  - landscape tablet
      920px  - large tablet
      768px  - tablet
      640px  - large phone / small tablet
      480px  - phone
      ────
     min-width: 640px is used for grid auto-fill enable-only
*/

/* ── Large desktop (1200px) ── */
@media (width <= 1200px) {


    .dashboard-row[style*="1fr 1fr 1fr"] { grid-template-columns: 1fr 1fr !important; }
    .dashboard-row[style*="repeat(3"] { grid-template-columns: repeat(2, 1fr) !important; }
    .dashboard-row[style*="2fr 1fr"] { grid-template-columns: 1fr 1fr !important; }
}

/* ── Landscape tablet (1024px) ── */
/* (the `.grid-2 { 1fr 1fr }` override that lived here was removed: with no
   lower bound it forced two columns down onto phones, fighting the mobile-first
   single-column rule. The Grid Utilities auto-fit rule already yields two
   columns on tablet widths and collapses to one below 640px.) */

/* ── Large tablet (920px) ── */
@media (width <= 920px) {
    /* -- App layout -- */
    .app-footer-tagline { display: none; }
    /* -- Pipeline & Chat -- */
    .pipeline { gap: 0.75rem; }
    .pipeline-col { min-width: 200px; }
    /* chat-layout width handled by clamp() on the base rule */
    /* -- Reports -- */
    /* :not(.kpi-strip) so the chip-strip variant keeps its
       auto-fit + minmax responsive grid (defined upstream on .kpi-strip).
       Without the guard, this hardcoded 2-col override at <=1200px
       collapsed every section's chip strip to 2 fat columns. */
    .report-kpis:not(.kpi-strip) { grid-template-columns: repeat(2, 1fr); }
}

/* ── Tablet (768px) ── */
@media (width <= 768px) {
    /* -- Forms: iOS input zoom prevention -- */
    input, select, textarea { font-size: 16px; }
    /* -- Touch targets (WCAG 2.1 - 44px minimum) -- */
    .btn-icon, .btn-sm, .topbar-icon-btn, .btn-delete,
    .tabs a, .sort-link {
        min-height: 44px; min-width: 44px;
        display: inline-flex; align-items: center; justify-content: center;
    }
    /* Sidebar nav gets the 44px touch-target bump WITHOUT the display:
       inline-flex + justify-content: center treatment - it's a vertical
       list where each item is a full-width row (display: flex, left-aligned
       icon+label). Inline-flex would wrap items side-by-side. */
    .sidebar-nav a { min-height: 44px; }
    /* :not() exclusions match the global button-reset chain (lines
       620/639/645) so chrome elements that are technically <button>
       but shouldn't be touch-target-sized stay at their design size:
         .form-sections-dot   8px pagination dot - stretched to 40px
                              read as a vertical pill on mobile
         .form-sections-arrow 32px page-nav arrow - similar
         .star-btn / .emoji-btn / .rec-btn / .eff-btn  custom survey
                              answer affordances with their own sizing
         .slot-row            scheduling slot picker row
         .ec-button, [class*=ec-]  Event Calendar internals
         [class*=leaflet-]    Leaflet map controls */
    button:not(.form-sections-dot):not(.form-sections-arrow):not(.star-btn):not(.emoji-btn):not(.rec-btn):not(.eff-btn):not(.slot-row):not(.ec-button):not([class*="ec-"]):not([class*="leaflet-"]),
    [role="button"]:not([class*="ec-"]):not([class*="leaflet-"]),
    a[role="button"] { min-height: 40px; }
    .btn-sm { min-height: 36px; }
    /* -- Topbar -- */
    .top-bar { padding: 0 0.875rem; }
    .top-bar-right { gap: 0.375rem; }
    /* -- Sidebar: off-canvas on mobile -- */
    .sidebar { transform: translateX(-100%); z-index: var(--z-tooltip); width: var(--sidebar-width) !important; }
    .sidebar.open { transform: translateX(0); }
    .sidebar-collapsed .sidebar .nav-label { display: inline !important; }
    /* Mobile overlay: even if the user had collapsed the sidebar on desktop,
       the overlay expands to full width - so mirror the expanded nav row
       layout (full-width bg, left stripe, no pill radius). */
    .sidebar-collapsed .sidebar-nav a { justify-content: flex-start !important; padding: 0.55rem 1.25rem !important; margin: 0 !important; border-left: 3px solid transparent !important; border-radius: 0 !important; }
    .sidebar-collapsed .sidebar-nav a.active { border-left-color: var(--primary) !important; }
    .sidebar-collapsed .sidebar-brand { justify-content: flex-start !important; padding: 1.25rem 1.5rem !important; }
    .sidebar-collapsed .sidebar-brand .nav-label { display: inline !important; }
    .sidebar-collapsed .sidebar-nav .nav-section::after { display: none; }
    .sidebar-toggle { display: none; }
    .main-content { margin-left: 0 !important; }
    .main-body { padding: 1rem; }
    .sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(43, 44, 64, 0.5); z-index: var(--z-overlay); }
    .sidebar-overlay.open { display: block; }
    /* Admin nav toggle becomes available when the sidebar is off-canvas. */
    .admin-menu-btn { display: inline-flex; }
    /* -- Section headers -- */
    .section-header { justify-content: flex-start !important; flex-wrap: nowrap; gap: 0.5rem; }
    .section-header h2, .section-header h3, .section-header h4 { flex: 0 1 auto !important; text-align: left; min-width: 0; }
    .section-header .btn-pill { flex: 0 0 auto; }
    /* -- Tables -- */
    thead th { padding: 0.5rem 0.75rem; font-size: var(--text-xs); }
    tbody td { padding: 0.5rem 0.75rem; font-size: var(--text-sm); }
    .table-actions { gap: 0.5rem; }
    .table-actions .btn-icon { min-width: 40px; min-height: 40px; }
    /* -- Tabs wrap -- */
    .tabs { flex-wrap: wrap; gap: 0.25rem; }
    /* -- Calendar -- */
    #calendar-card, .sidebar-collapsed #calendar-card { max-width: 100%; }
    /* -- Dashboard grids stack -- */
    .grid-2 { grid-template-columns: 1fr !important; }
    .dashboard-row[style*="1fr 1fr 1fr"] { grid-template-columns: 1fr !important; }
    .dashboard-row[style*="repeat(3"] { grid-template-columns: 1fr !important; }
    .dashboard-row[style*="2fr 1fr"] { grid-template-columns: 1fr !important; }
    /* 2-col dashboard rows (Revenue + Activity Feed at 260px) stack on mobile
       so the narrow Activity Feed column doesn't break each entry into 3-4
       lines. :not() excludes the 3-col pattern already handled above. */
    .dashboard-row[style*="1fr 1fr"]:not([style*="1fr 1fr 1fr"]) { grid-template-columns: 1fr !important; }
    /* -- Reports -- */
    .report-kpis:not(.kpi-strip) { grid-template-columns: 1fr 1fr; }
    .report-section-body { padding: 1rem; }
    .profitability-row { flex-direction: column; }
    /* -- Map View on mobile: map first, list below --
       Pre-fix used column-reverse to put the job list on top, which
       buried the actual Leaflet map below the fold (page title says
       "Job Map" - customers reasonably expect to see the map without
       scrolling). Switched to natural column order so the map sits
       at the top + the list scrolls below it.

       #leaflet-map gets an explicit min-height in addition to height:
       100% because the height:100% chain breaks when its grandparent
       .map-layout is height:auto - the inner div was resolving to 0
       even though .map-container had its own min-height of 280. */
    .map-layout { flex-direction: column; height: auto; }
    .map-container { height: 50vh; min-height: 320px; }
    #leaflet-map { min-height: 320px; }
    .map-sidebar { max-height: 360px; }
    .detail-layout { gap: 0.75rem; }
    /* Tabs-vertical stays vertical but switches to icon + count-number mode.
       Label text hides (font-size: 0) while .tab-count keeps its own size so
       the "(3)" badge stays visible stacked under the icon. Width continues
       to scale via clamp() on the base rule (floor 48px). */
    .tabs-vertical a, .tabs-vertical .tab-btn {
        padding: 0.45rem 0.2rem;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        font-size: 0;
        gap: 2px;
    }
    .tabs-vertical a i, .tabs-vertical .tab-btn i,
    .tabs-vertical a svg, .tabs-vertical .tab-btn svg { width: 18px; height: 18px; font-size: var(--text-body); flex-shrink: 0; }
    .tabs-vertical .tab-count { margin-left: 0; padding: 0 0.3rem; min-width: 1rem; font-size: var(--text-xs); line-height: 1.2; }
    /* (Bulk action bar mobile styling lives in one place: the <=640px block
       near the .bulk-action-bar base rules. The duplicate column/center rules
       that used to sit here + in the <=480px block fought it and lacked
       flex-wrap:nowrap, so the bar still overflowed.) */
    /* -- KPI / Timeline -- */
    .kpi-card { padding: 1rem; }
    .kpi-value { font-size: var(--step-2); }
    .timeline { padding-left: 0.75rem; }
    .timeline-entry { margin-bottom: 0.75rem; }
    /* -- Footer: tight single-line brand + help, pinned to bottom of the
       dvh-sized main-content so it stays visible on mobile even when the
       page has little content. margin-top:auto replaces the prior fixed
       margin so the flex column pushes it to the bottom; padding-bottom
       adds the iOS safe-area inset so the home indicator doesn't overlap.
       Using dvh on main-content (already set) means the footer stays within
       the visible viewport as mobile browser chrome shows/hides. -- */
    .app-footer {
        margin-top: auto;
        padding: 0.5rem 1rem max(0.5rem, env(safe-area-inset-bottom));
        flex-direction: row;
        justify-content: space-between;
        gap: 0.5rem;
        font-size: var(--text-xs);
    }
    .app-footer-top { gap: 0.35rem; }
    .app-footer-tagline { display: none; }
    .app-footer-logo { padding: 0.25rem 0.5rem; }
    .app-footer-logo img { height: 12px; }
    .app-footer-name { font-size: var(--text-xs); }
    .app-footer-help { font-size: var(--text-xs); }
}

/* ── Large phone / small tablet (640px) ── */
@media (width <= 640px) {
    /* -- Page header stacks -- */
    .page-header { flex-direction: column; align-items: flex-start; padding: 0; gap: 0.5rem; }
    /* Stacked: the title block owns the full row it now sits on. The fix that
       lets a long title/subtitle WRAP is container-relative (it overflows
       whenever the content area is narrower than the content, sidebar included -
       not just on phones), so it lives in the base rules, not behind this
       viewport breakpoint. */
    .page-header > div:first-child { width: 100%; }
    /* The <page-header> tag helper renders its actions in a class-LESS div, so
       the .actions selector alone never reached it - a header packed with
       buttons (Jobs: Calendar/Map/Routes/New + kebab) overflowed. Target the
       tag-helper slot too so it wraps full-width on phones. */
    .page-header .actions,
    .page-header > div:not(:first-child):last-child { flex-wrap: wrap; width: 100%; gap: 0.5rem; }
    .page-header .actions > *,
    .page-header > div:not(:first-child):last-child > * { flex-shrink: 0; }
    /* Admin topbar: the breadcrumb already reads "System Admin", so the identity
       name label is redundant and pushes the right cluster past a phone
       viewport. Drop it (and its divider) so the sign-out icon stays on-screen;
       let the breadcrumb title truncate rather than overflow. */
    .admin-area .admin-identity-name { display: none; }
    .admin-area .admin-identity { padding-left: 0; border-left: 0; gap: 0; }
    /* flex-shrink:1 is the fix: the base .top-bar-left is flex-shrink:0, so it
       kept its full width and squeezed the right cluster (theme + sign-out) to
       a few px, whose icons then overflowed off-screen. Letting it shrink lets
       the breadcrumb title truncate and the right cluster stay on-screen. */
    .admin-area .top-bar-left { min-width: 0; flex-shrink: 1; }
    .admin-area .admin-breadcrumb { min-width: 0; }
    .admin-area .admin-breadcrumb strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    /* -- Section header stacks -- */
    .section-header { flex-direction: column; align-items: flex-start; flex-wrap: wrap; row-gap: 0.5rem; }
    .section-header > div:last-child:not([popover]),
    .section-header > :last-child:not(h2):not(h3):not(h4):not([popover]) { width: 100%; flex-wrap: wrap; }
    /* heading flex handled by the <=768px rule above (covers <=640 too) */
    .section-header .btn-pill { flex: 0 0 auto; }
    /* -- Buttons / forms -- */
    .btn-sm { min-height: 40px; }
    .form-actions { flex-direction: column; }
    .form-actions button, .form-actions a { width: 100%; justify-content: center; }
    /* (card padding is fluid via --pad-card now - no mobile step needed) */
    /* -- Chat layout: single-column with sidebar↔conversation toggle. --
       Default on mobile: show the list (sidebar), hide the conversation.
       Clicking a contact sets data-view="conversation" on .chat-layout,
       which swaps visibility. A ".chat-back" button in .chat-main header
       (hidden on desktop) returns to the list by resetting data-view. */
    .chat-layout { grid-template-columns: 1fr; }
    .chat-layout .chat-sidebar { display: flex; }
    .chat-layout .chat-main { display: none; }
    .chat-layout[data-view="conversation"] .chat-sidebar { display: none; }
    .chat-layout[data-view="conversation"] .chat-main { display: flex; }
    button.chat-back { display: inline-flex !important; }
    /* -- Pipeline stacks -- */
    .pipeline { flex-direction: column; }
    .pipeline-col { min-width: 0; width: 100%; }
    /* -- Form grid tighter -- */
    .form-grid { gap: 0.5rem 0.75rem; }
    /* -- Modal sizing: the base rule already caps at calc(100vw - 2rem) and
       centers via translate. Loosen min-width so forms can shrink on tablets. -- */
    dialog { min-width: 0; padding: 1.125rem; }
}

/* ── Phone (480px) ── */
@media (width <= 480px) {
    /* -- Topbar very compact -- */
    .top-bar { padding: 0 0.625rem; }
    .top-bar-right { gap: 0.25rem; }
    .top-bar .topbar-icon-btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; }
    /* -- Cards / inputs compact -- */
    .card-compact { padding: var(--pad-card-tight); }
    /* -- Detail / main content tighter -- */
    .detail-layout > div:last-child { padding: 0.875rem; }
    .main-body { padding: 0.75rem; }
    /* Tabs-vertical icon-only mode is set up by the 768px block above (font-size: 0
       to hide labels, stacked icon + .tab-count). No per-link override here - a
       prior rule set font-size: var(--text-sm), which overrode the icon-only
       hide and caused labels to bleed through the content panel. */
    /* -- KPI -- */
    .kpi-card { padding: 0.875rem; }
    .kpi-value { font-size: clamp(1rem, 0.75rem + 1vw, 1.5rem); }
    /* -- Page header -- */
    .page-header { gap: 0.5rem; }
    .page-header .actions > * { flex: 1 1 auto; }
    /* -- Overflow containment -- */
    .toast { max-width: calc(100vw - 2rem); margin: 0 1rem; }
    .ec-tooltip { max-width: calc(100vw - 2rem); }
    /* Mobile: the base rule already centers the popup and caps max-width at
       calc(100vw - 2rem); only override the min-width so very small phones
       aren't forced to a 520px layout that would overflow the viewport. */
    .popover-panel { min-width: 0; width: calc(100vw - 2rem); }
    /* -- Pagination -- */
    nav[aria-label="Pagination"] ul { gap: 0.25rem; flex-wrap: wrap; justify-content: center; }
    /* -- Modal compact -- */
    dialog { padding: 0.875rem; }
    /* -- Empty state / table actions -- */
    .empty-state-rich { padding: 2rem 1rem; }
    .table-actions { gap: 0.25rem; flex-wrap: wrap; }
    /* -- Reports -- */
    .report-kpis:not(.kpi-strip) { grid-template-columns: 1fr; }
}

/* ---- Deleted-customer banner ------------------------------------------
   Uses --warning tokens + color-mix so the banner reads correctly in
   both themes from a single rule (no separate dark-mode override). */
.deleted-customer-banner {
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    border: 1px solid var(--warning);
    color: color-mix(in srgb, var(--warning) 75%, var(--heading));
    padding: 0.65rem 1rem; border-radius: var(--radius-sm); margin: 0 0 1rem;
    font-size: var(--text-sm); display: flex; align-items: center; gap: 0.5rem;
}
.deleted-customer-banner .banner-icon { font-size: var(--step-3); }
[data-theme="dark"] .deleted-customer-banner { color: var(--warning); }

/* ---- Demo top-bar -------------------------------------------------------- */
.demo-banner-link { color: #fff; text-decoration: underline; font-weight: var(--fw-semibold); font-variation-settings: 'wght' var(--fw-semibold); }

/* ---- Gallery (image-preview-dialog) ------------------------------------- */
/* Horizontal scroll-snap track: one slide per image, full viewport width each,
   so a swipe is native momentum scroll that snaps to the next photo. */
.gallery-track {
    display: flex;
    width: 100%;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    touch-action: pan-x;
    scrollbar-width: none;          /* Firefox - hide the scrollbar */
}
.gallery-track::-webkit-scrollbar { display: none; }   /* WebKit - hide the scrollbar */

.gallery-slide {
    flex: 0 0 100%;
    width: 100%;
    height: 100%;
    margin: 0;
    box-sizing: border-box;
    padding: 2rem;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1rem;
}
.gallery-slide-img {
    max-width: 100%;
    max-height: 78dvh;
    object-fit: contain;
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.gallery-caption {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    text-align: center;
    max-width: min(640px, 90vw);
}
.gallery-caption-desc { color: #fff; font-size: var(--text-base); text-shadow: 0 1px 4px rgba(0,0,0,0.6); }
.gallery-caption-meta { color: rgba(255,255,255,0.75); font-size: var(--text-sm); }
.gallery-caption-date { color: rgba(255,255,255,0.5); font-size: var(--text-xs); }
.gallery-caption-link { color: rgba(255,255,255,0.85); font-size: var(--text-xs); margin-top: 0.25rem; }

/* Overlay controls - positioned against the full-screen dialog, never in the
   image's flow, so the photo always centers. */
.gallery-arrow {
    position: absolute; top: 50%; transform: translateY(-50%);
    background: rgba(0,0,0,0.5); border: none; color: #fff;
    width: 44px; height: 44px; border-radius: 50%; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: var(--step-3); z-index: 2;
}
.gallery-arrow--prev { left: 0.75rem; }
.gallery-arrow--next { right: 0.75rem; }
.gallery-close {
    position: absolute; top: 0.75rem; right: 0.75rem;
    background: rgba(0,0,0,0.5); border: none; color: #fff;
    width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: var(--step-3); z-index: 3;
}
.gallery-counter {
    position: absolute; bottom: 0.75rem; left: 50%; transform: translateX(-50%);
    color: rgba(255,255,255,0.85); font-size: var(--text-sm);
    background: rgba(0,0,0,0.4); padding: 0.15rem 0.65rem; border-radius: var(--radius-pill);
    z-index: 2;
}

/* On phones the swipe IS the navigation, so drop the edge arrows (they'd sit on
   top of the photo) and give the image a touch more room. */
@media (width <= 768px) {
    .gallery-arrow { display: none; }
    .gallery-slide { padding: 1rem; gap: 0.6rem; }
    .gallery-slide-img { max-height: 72dvh; }
}


/* ---- Leaflet chrome theming --------------------------------------------
   Leaflet ships hardcoded white-on-dark-border defaults for every UI
   control - popup balloon, zoom +/- buttons, attribution bar, layers
   toggle. We re-theme each surface against --card / --text so a single
   ruleset works in both light AND dark mode automatically.

   No specificity gymnastics needed - leaflet.min.css lives in @layer
   vendor (declared at top of file, injected via loadLib() in _Layout),
   so any unlayered rule below wins regardless of selector specificity
   or source order. */

/* Popup balloon + tip + close button */
.leaflet-popup-content-wrapper { background: var(--card); color: var(--text); border-radius: var(--radius); box-shadow: var(--shadow-md); }
.leaflet-popup-tip { background: var(--card); box-shadow: var(--shadow-md); }
.leaflet-popup-close-button { color: var(--text-muted); }
.leaflet-popup-close-button:hover { color: var(--text); }

/* Zoom +/- buttons and any other .leaflet-bar controls. All four states
   covered explicitly so post-click :focus doesn't fall back to Leaflet's
   #f4f4f4 light gray. */
.leaflet-bar { border: 1px solid var(--border); box-shadow: var(--shadow-sm); background: var(--card); }
.leaflet-bar a { background: var(--card); color: var(--text); border-bottom-color: var(--border); }
.leaflet-bar a:hover,
.leaflet-bar a:focus,
.leaflet-bar a:active { background: var(--bg); color: var(--primary); outline: none; }
.leaflet-bar a:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.leaflet-bar a.leaflet-disabled { background: var(--card); color: var(--text-muted); cursor: not-allowed; }

/* Attribution bar bottom-right */
.leaflet-control-attribution { background: color-mix(in srgb, var(--card) 88%, transparent); color: var(--text-muted); }
.leaflet-control-attribution a { color: var(--primary); }

/* Layers control toggle + panel (when a layer switcher is added) */
.leaflet-control-layers { background: var(--card); color: var(--text); border: 1px solid var(--border); box-shadow: var(--shadow-sm); }
.leaflet-control-layers-toggle { background-color: var(--card); }
.leaflet-control-layers-separator { border-top-color: var(--border); }

/* ---- MapView marker and popup type -------------------------------------- */

.map-marker-avatar { font-size: var(--text-xs); font-weight: var(--fw-bold); font-variation-settings: 'wght' var(--fw-bold); color: #fff; display: flex; align-items: center; justify-content: center; }
.map-popup-badge   { display: inline-block; padding: 0.1rem 0.4rem; border-radius: 4px; font-size: var(--text-xs); font-weight: var(--fw-semibold); font-variation-settings: 'wght' var(--fw-semibold); color: #fff; }
.map-popup-meta    { font-size: var(--text-sm); color: var(--text); }
.map-popup-link    { font-size: var(--text-sm); display: inline-block; margin-top: 0.35rem; }
.map-popup-schedule{ font-size: var(--text-sm); color: var(--primary); font-weight: var(--fw-medium); font-variation-settings: 'wght' var(--fw-medium); }

/* Count badge for stacked markers (multiple jobs at
   one lat/lng). Sits in the top-right corner of the 28x28 avatar with
   a white ring so it reads against any color underneath. The cluster
   popup row inherits the same compact spacing as the single-job popup. */
.map-marker-count-badge {
    position: absolute;
    top: -6px;
    right: -6px;
    min-width: 18px;
    height: 18px;
    padding: 0 0.25rem;
    border-radius: 9px;
    background: var(--danger);
    color: #fff;
    border: 2px solid var(--white);
    font-size: 10px;
    font-weight: var(--fw-bold);
    line-height: 14px;
    text-align: center;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.map-popup-cluster-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--text-sm);
    padding: 0.15rem 0;
    border-bottom: 1px solid var(--border);
}
.map-popup-cluster-row:last-child { border-bottom: none; }

/* ════════════════════════════════════════════════════════════
   HELP WIKI
   ════════════════════════════════════════════════════════════
   /help/* surfaces; gradient hero, TOC sidebar, article shell,
   FAQ accordion, screenshot/mockup frames, process tiles,
   glossary grid, search results, and article pager footer.

   Pre-merge this lived in a separate help-wiki.css that was
   <link>-loaded by 4 views (Article / Category / Search /
   NotFound). Merging cost ~30 KB per non-help page load in
   exchange for one stylesheet to maintain + dark-mode rules
   cascading from a single source.

   Audit pass during merge:
     • `.help-content` body + heading rules deleted; prose now
       lives on the shared `.article` class above.
     • `.help-screenshot img` carries the same border/shadow as
       the `.article img` rule; kept distinct only so the
       wrapper figcaption styling has somewhere to anchor.
     • `.help-help` (read like a typo) renamed to `.help-prompt`
      ; it's the "didn't find it?" footer block; the old name
       conflicted with no other utility but read poorly.
     • Dark-mode `[data-theme="dark"] a.help-x:not(...)` chains
       are preserved because they have to outrank the global
       `[data-theme="dark"] a:not(...)` rule above. The class-
       bearing selectors carry one more class-weight unit, so
       source order is irrelevant; they win on specificity.
     • Follow-up: `.kb-toc-list` (KB article TOC) and `.help-toc`
       (Help category TOC) implement similar sticky-sidebar
       chrome from different data models. A `.toc-*` family
       could absorb both later; out of scope for this pass.
   ──────────────────────────────────────────────────────────── */

.help-wiki {
    background: var(--bg);
    min-height: 100vh;
    /* Help renders in its own standalone shell (_HelpLayout), flush to the
       viewport - no app content-area padding to break out of, so margin: 0. */
    margin: 0;
    padding: 0;
}

/* ── Hero (gradient header with search + back-to-app) ── */
.help-hero {
    background-color: var(--primary);
    background-image: linear-gradient(135deg, var(--primary) 0%, #4a4dc8 100%);
    color: #fff;
    padding: clamp(0.9rem, 1.2vw + 0.5rem, 1.5rem) clamp(0.85rem, 2vw + 0.4rem, 1.6rem);
    box-shadow: var(--shadow-sm);
}
.help-hero-inner {
    max-width: 1280px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: 1.25rem;
    align-items: center;
}

/* Brand and back-to-app are nav links inside the purple hero, so they
   need explicit white. The :not() chain matches the global dark-mode
   link rule above to win on specificity (extra class-weight unit). */
.help-brand,
[data-theme="dark"] a.help-brand:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    color: #fff;
    text-decoration: none;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--step-2);
    letter-spacing: -0.01em;
}
.help-brand i { width: 22px; height: 22px; }

.help-back-app,
[data-theme="dark"] a.help-back-app:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: rgba(255,255,255,0.92);
    text-decoration: none;
    font-size: var(--text-sm);
    padding: 0.4rem 0.8rem;
    border-radius: var(--radius-sm);
    transition: background 0.15s;
}
.help-back-app:hover { background: rgba(255,255,255,0.15); color: #fff; }
.help-back-app i { width: 16px; height: 16px; }

.help-search-form {
    position: relative;
    display: flex;
    align-items: center;
    background: #fff;
    border-radius: var(--radius-pill);
    padding: 0 1.1rem;
    box-shadow: 0 4px 14px rgba(0,0,0,0.18);
    min-width: 0;
    transition: box-shadow 0.15s;
}
.help-search-form:focus-within {
    box-shadow: 0 4px 14px rgba(0,0,0,0.18), 0 0 0 3px rgba(255,255,255,0.25);
}
.help-search-icon {
    display: inline-flex;
    align-items: center;
    color: var(--text-muted);
    margin-right: 0.6rem;
    pointer-events: none;
    flex-shrink: 0;
}
.help-search-icon i { width: 18px; height: 18px; }

.help-search-input {
    flex: 1 1 0;
    min-width: 0;
    border: 0;
    outline: none;
    padding: 0.7rem 0;
    font: inherit;
    font-size: var(--text-body);
    background: transparent;
    color: var(--heading);
}
.help-search-input::placeholder { color: var(--text-muted); }

[data-theme="dark"] .help-search-form { background: #2A2C48; }
[data-theme="dark"] .help-search-input { color: #f0f1f8; }
[data-theme="dark"] .help-search-input::placeholder { color: #8a8da6; }
[data-theme="dark"] .help-search-icon { color: #8a8da6; }

/* ── Shell (TOC + article columns) ── */
.help-shell {
    display: grid;
    grid-template-columns: clamp(240px, 22vw, 300px) 1fr;
    gap: 2rem;
    max-width: 1280px;
    margin: 0 auto;
    padding: 2rem 1.25rem 4rem;
    align-items: start;
}
/* View Transitions target is the article pane ONLY (#help-main), matching the
   HTMX swap target. So a help nav crossfades just the article - the TOC, hero,
   and app shell are siblings with no transition name and stay steady. Naming
   .help-shell here instead faded the whole TOC+article block = a page blink. */
#help-main { view-transition-name: help-main; }
::view-transition-old(help-main) { animation: 100ms ease-out both vtFadeOut; }
::view-transition-new(help-main) { animation: 100ms ease-in both vtFadeIn; }
/* The article column is a plain .card now; min-width:0 lets the 1fr grid
   track shrink so long <pre>/tables wrap instead of blowing out the column. */
.help-shell > article { min-width: 0; }

/* ── TOC sidebar ── */
.help-toc {
    position: sticky;
    top: 1rem;
    max-height: calc(100dvh - 2rem);
    overflow-y: auto;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 0.85rem;
    box-shadow: var(--shadow-sm);
}
.help-toc-mobile { display: none; }

.help-toc-home,
[data-theme="dark"] a.help-toc-home:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.65rem;
    border-radius: var(--radius-sm);
    color: var(--heading);
    text-decoration: none;
    font-weight: 600;
    font-size: var(--text-sm);
    margin-bottom: 0.5rem;
}
.help-toc-home:hover,
.help-toc-home.is-active,
[data-theme="dark"] a.help-toc-home.is-active:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    background: var(--primary-light);
    color: var(--primary);
}
.help-toc-home i { width: 16px; height: 16px; }

.help-toc-group { margin-bottom: 0.35rem; }
.help-toc-group > summary {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.65rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
    color: var(--heading);
    font-weight: 600;
    font-size: var(--text-sm);
    list-style: none;
    user-select: none;
}
.help-toc-group > summary::-webkit-details-marker { display: none; }
.help-toc-group > summary::marker { content: ""; }

/* Defeat the global summary +/- pseudo on TOC nav rows. */
details.help-toc-group > summary::after,
details[open].help-toc-group > summary::after { content: none; }
.help-toc-group[open] > summary {
    background: transparent;
    color: var(--heading);
}
.help-toc-group > summary:hover,
.help-toc-group[open] > summary:hover {
    background: color-mix(in srgb, var(--primary) 6%, transparent);
    color: var(--heading);
}

.help-toc-group > summary > i:first-child {
    width: 16px; height: 16px;
    color: var(--text-muted);
    flex-shrink: 0;
}
.help-toc-label { flex: 1 1 auto; min-width: 0; }

.help-toc-chevron {
    width: 14px; height: 14px;
    color: var(--text-muted);
    transition: transform 0.2s ease;
    flex-shrink: 0;
}
.help-toc-group[open] > summary .help-toc-chevron { transform: rotate(90deg); }

.help-toc-count {
    background: var(--border);
    color: var(--text-muted);
    font-size: var(--text-xs);
    padding: 0.05rem 0.45rem;
    border-radius: var(--radius-pill);
    font-weight: 500;
    margin-left: 0.4rem;
}

.help-toc-group ul {
    list-style: none;
    padding: 0.25rem 0 0.5rem 1.7rem;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.05rem;
}
.help-toc-group li a,
[data-theme="dark"] .help-toc-group li a:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    display: block;
    padding: 0.32rem 0.55rem;
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
    font-size: var(--text-sm);
    line-height: 1.4;
}
.help-toc-group li a:hover {
    background: color-mix(in srgb, var(--primary) 6%, transparent);
    color: var(--heading);
}
.help-toc-group li a.is-active,
[data-theme="dark"] .help-toc-group li a.is-active:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    background: var(--primary-light);
    color: var(--primary);
    font-weight: 600;
}

/* ── Article shell ── */
.help-breadcrumb {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    font-size: var(--text-sm);
    color: var(--text-muted);
    margin-bottom: 1rem;
    flex-wrap: wrap;
}
.help-breadcrumb a,
[data-theme="dark"] .help-breadcrumb a:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    color: var(--text-muted);
    text-decoration: none;
}
.help-breadcrumb a:hover { color: var(--primary); text-decoration: underline; }
.help-breadcrumb-sep { color: var(--border); }

.help-article-header {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 1rem;
    align-items: start;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 1.75rem;
}

.help-article-header h1 {
    font-family: var(--font-display);
    margin: 0 0 0.4rem;
    /* Step-3 (18→22 px) matches the app's page-title convention
       (.page-header h2 also at --step-3). */
    font-size: var(--step-3);
    line-height: var(--lh-heading);
    color: var(--heading);
}
.help-article-summary {
    margin: 0;
    /* --text-md (15→16 px) reads as a lead one notch under body. */
    font-size: var(--text-body);
    color: var(--text-muted);
    line-height: var(--lh-body);
}

.help-persona-badge {
    display: inline-block;
    margin-top: 0.6rem;
    background: var(--primary-light);
    color: var(--primary);
    padding: 0.2rem 0.7rem;
    border-radius: var(--radius-pill);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

/* ── Embedded media frames ── */


/* Inline browser-frame mockup */
.help-mockup {
    margin: 1.4rem 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}
.help-mockup-bar {
    background: var(--card);
    padding: 0.55rem 0.85rem;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--text-xs);
    color: var(--text-muted);
}
.help-mockup-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border); }
.help-mockup-dot:nth-child(1) { background: #FF5F57; }
.help-mockup-dot:nth-child(2) { background: #FEBC2E; }
.help-mockup-dot:nth-child(3) { background: #28C840; }
.help-mockup-bar > span { margin-left: 0.6rem; }
.help-mockup-body { padding: 1.25rem; font-size: var(--text-sm); }

/* ── FAQ accordion (defeats the global +/- summary pseudo) ── */
.help-faq { display: grid; gap: 0.75rem; margin: 1rem 0; }
.help-faq details {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--card);
    padding: 0;
    overflow: hidden;
}
.help-faq summary {
    list-style: none;
    cursor: pointer;
    padding: 0.85rem 1.05rem;
    font-weight: 600;
    color: var(--heading);
    background: transparent;
    display: flex;
    justify-content: space-between;
    align-items: center;
    user-select: none;
    border-radius: var(--radius);
    transition: background 0.15s;
}
.help-faq summary:hover {
    background: color-mix(in srgb, var(--primary) 6%, transparent);
    color: var(--heading);
}
.help-faq summary::-webkit-details-marker { display: none; }
.help-faq summary::after {
    content: "+";
    color: var(--text-muted);
    font-size: var(--step-2);
    line-height: 1;
}
.help-faq details[open] > summary {
    background: transparent;
    color: var(--heading);
    border-bottom: 1px solid var(--border);
    border-radius: var(--radius) var(--radius) 0 0;
}
.help-faq details[open] > summary:hover {
    background: color-mix(in srgb, var(--primary) 6%, transparent);
}
.help-faq details[open] > summary::after { content: "−"; color: var(--primary); }
.help-faq details > div {
    padding: 0 1.05rem 1rem;
    color: var(--text);
    line-height: 1.6;
}

/* ── Landing helpers, glossary, empty state ── */
.help-empty {
    text-align: center;
    padding: 2.5rem 1rem;
}
.help-empty-icon { width: 48px; height: 48px; color: var(--text-muted); margin-bottom: 0.6rem; }
.help-empty h3 { margin: 0.4rem 0 0.4rem; color: var(--heading); font-family: var(--font-display); }
.help-empty p { color: var(--text-muted); margin: 0.4rem 0; }

.help-landing-hero {
    text-align: center;
    padding: 1.5rem 0 2.25rem;
}
.help-landing-hero h2 {
    font-family: var(--font-display);
    /* Step-3 matches the page-title convention. */
    font-size: var(--step-3);
    color: var(--heading);
    margin: 0 0 0.5rem;
    line-height: var(--lh-heading);
}
.help-landing-hero p {
    font-size: var(--text-body);
    color: var(--text);
    max-width: 640px;
    margin: 0.5rem auto 0;
    line-height: 1.6;
}

.help-section-title {
    font-family: var(--font-display);
    font-size: var(--step-3);
    color: var(--heading);
    margin: 2rem 0 0.4rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.help-section-title i { color: var(--primary); width: 22px; height: 22px; }
.help-section-sub { color: var(--text-muted); margin: 0 0 1rem; }

.help-glossary-grid { display: grid; gap: 0.7rem; margin: 1rem 0; }
.help-glossary-grid dt { color: var(--heading); font-weight: 700; margin-top: 0.5rem; }
.help-glossary-grid dd { margin: 0.1rem 0 0.4rem; color: var(--text); line-height: 1.6; }

/* ── Process-tile grid (embedded inside article body) ── */
.help-process-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr));
    gap: 0.75rem;
    margin: 1.25rem 0 1.5rem;
}
.help-process-tile {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.85rem 1rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    text-decoration: none;
    transition: var(--transition-fast, 120ms) ease;
}
/* Outweigh the .article a (0,1,1) underline rule when these tiles are
   embedded in an article body. */
.article .help-process-tile,
.article .help-process-tile:hover { text-decoration: none; }
.help-process-tile:hover {
    border-color: var(--primary);
    background: var(--primary-light);
    transform: translateY(-1px);
}
.help-process-tile > i,
.help-process-tile > svg {
    flex: 0 0 auto;
    width: 22px; height: 22px;
    color: var(--primary);
}
.help-process-tile > div {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}
.help-process-tile strong {
    font-family: var(--font-display);
    font-weight: var(--fw-semibold);
    color: var(--heading);
    font-size: var(--text-sm);
}
.help-process-tile span {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* ── Article footer (pager + "didn't find it?" prompt) ── */
.help-article-foot {
    margin-top: 2.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--border);
    display: grid;
    gap: 1.5rem;
}
.help-pager {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.85rem;
}

/* "Didn't find what you were looking for?" footer block.
   Renamed from .help-help during the merge; old name parsed as a typo. */
.help-prompt {
    text-align: center;
    padding: 1.5rem 1rem;
    background: var(--bg);
    border-radius: var(--radius);
}
.help-prompt p { color: var(--text-muted); margin: 0 0 0.7rem; }
.help-prompt-actions { display: inline-flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.help-prompt-actions .btn { display: inline-flex; align-items: center; gap: 0.4rem; }

/* ── Search results list ── */
.help-search-results {
    list-style: none;
    padding: 0;
    margin: 1rem 0 0;
    border-top: 1px solid var(--border);
}
.help-search-results > li {
    margin: 0;
    padding: 0;
}
/* Global search palette (Ctrl+K) results scroller. The palette is built in
   app.js; this lifts its one purely-static inline style into a class. */
.global-search-results { max-height: 400px; overflow-y: auto; }
.help-search-row {
    display: grid;
    grid-template-columns: 32px 1fr auto;
    grid-template-rows: auto auto;
    grid-template-areas:
        "icon title    cat"
        "icon snippet  snippet";
    column-gap: 0.95rem;
    row-gap: 0.3rem;
    padding: 0.95rem 1rem;
    border-bottom: 1px solid var(--border);
    color: var(--text);
    text-decoration: none;
    transition: background var(--transition-fast, 120ms) ease;
}
.help-search-row:hover {
    background: color-mix(in srgb, var(--primary) 5%, transparent);
}
.help-search-row:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
    border-radius: var(--radius-sm);
}
.help-search-row-icon {
    grid-area: icon;
    width: 32px; height: 32px;
    display: grid;
    place-items: center;
    color: var(--primary);
    background: var(--primary-light);
    border-radius: var(--radius-sm);
    flex-shrink: 0;
}
.help-search-row-icon i,
.help-search-row-icon svg { width: 16px; height: 16px; }
.help-search-row-title {
    grid-area: title;
    font-family: var(--font-display);
    font-weight: var(--fw-semibold);
    color: var(--heading);
    font-size: var(--text-body);
    line-height: 1.3;
}
.help-search-row-cat {
    grid-area: cat;
    font-size: var(--text-xs);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    align-self: center;
    white-space: nowrap;
}
.help-search-row-snippet {
    grid-area: snippet;
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: 1.55;
}
.help-search-row-snippet mark {
    background: color-mix(in srgb, var(--warning) 35%, transparent);
    color: var(--heading);
    padding: 0 0.15rem;
    border-radius: 2px;
}

@media (width <= 560px) {
    .help-search-row {
        grid-template-columns: 28px 1fr;
        grid-template-areas:
            "icon title"
            "icon cat"
            "icon snippet";
        column-gap: 0.7rem;
        padding: 0.85rem 0.6rem;
    }
    .help-search-row-cat { align-self: start; }
}

/* ── Wiki overrides on the shared <info-card> primitive ──
   .info-card-title is line-clamped + reserves corner-badge space by default.
   For wiki nav tiles the title IS the link label and must always be fully
   legible, so we relax those rules inside .help-wiki only. */
.help-wiki .info-card-title {
    -webkit-line-clamp: unset;
    min-height: 0;
    display: block;
    padding-right: 0;
    font-size: var(--text-body);
}
.help-wiki .info-card:has(.info-card-badge) .info-card-title {
    padding-right: 3rem;
}
.help-wiki .info-card-body {
    color: var(--text);
}
/* The shared grid caps each card at 22rem so a lone card keeps its silhouette,
   but that makes auto-fit collapse to a single column in the wider standalone
   wiki shell, wasting the row. Wiki landing + category grids always carry
   several cards, so let them fill the row (1fr) at a legible 16rem min - two
   or three per row instead of one with dead space beside it. */
.help-wiki .info-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
/* The landing + category-index pages are card grids, not prose, so drop the
   .article 72ch reading cap on them (matched by their direct .info-card-grid
   child). The cards then use the full column - 3+ per row on a wide monitor -
   instead of sitting in a narrow centered band. Prose articles, which have no
   .info-card-grid child, keep the readable 72ch measure. */
.help-wiki .article:has(> .info-card-grid) {
    max-width: none;
}

/* ── Responsive ── */
@media (width <= 900px) {
    .help-hero-inner {
        grid-template-columns: 1fr;
        gap: 0.6rem;
    }
    .help-back-app { justify-self: end; }
    .help-shell {
        grid-template-columns: 1fr;
        gap: 1rem;
        padding: 1.25rem 0.85rem 3rem;
    }
    .help-toc { position: static; max-height: none; }
    .help-toc-mobile { display: block; margin-bottom: 0.65rem; }
    .help-toc-mobile summary {
        cursor: pointer;
        padding: 0.55rem 0.85rem;
        background: var(--bg);
        border-radius: var(--radius-sm);
        list-style: none;
        display: inline-flex;
        align-items: center;
        gap: 0.45rem;
        font-weight: 600;
        color: var(--heading);
        border: 1px solid var(--border);
    }
    .help-toc-mobile summary::-webkit-details-marker { display: none; }
    details.help-toc-mobile > summary::after,
    details[open].help-toc-mobile > summary::after { content: none; }
    .help-toc-mobile[open] > summary {
        background: var(--bg);
        color: var(--heading);
    }
    .help-toc-mobile summary:hover {
        background: color-mix(in srgb, var(--primary) 6%, var(--bg));
        color: var(--heading);
    }
    .help-toc-mobile:not([open]) ~ .help-toc-nav { display: none; }
    .help-pager { grid-template-columns: 1fr; }
    .help-article-header { grid-template-columns: 1fr; }
}

@media (width <= 380px) {
    .help-hero-inner { gap: 0.45rem; }
    .help-brand span { display: none; }
    .help-back-app span { display: none; }
    .help-shell { padding: 1rem 0.6rem 2.5rem; }
}

/* ── Public trade library (/library) ──
   Rides the help-wiki shell above (hero, breadcrumb, .card--doc, .article
   prose). The lib-* family is only what the library adds: browse grid,
   trade chips, article meta row, and the CTA band. Dark-mode link twins
   mirror the .help-brand pattern - they outrank the global dark-link rule
   on specificity so card titles/chips keep their own color. */
.lib-shell {
    max-width: 1160px;
    margin: 0 auto;
    padding: 1.75rem 1.25rem 4rem;
}
.lib-shell--article { max-width: 920px; }

.lib-intro h1 {
    font-size: var(--step-4);
    margin: 0 0 0.35rem;
}
.lib-intro p {
    color: var(--text-muted);
    margin: 0 0 1.1rem;
    max-width: 72ch;
}

.lib-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-bottom: 1.1rem;
}
a.lib-chip,
[data-theme="dark"] a.lib-chip:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.22rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--card);
    color: var(--text);
    font-size: var(--text-sm);
    text-decoration: none;
    transition: border-color 0.15s, color 0.15s;
}
a.lib-chip:hover { border-color: var(--primary); color: var(--primary); }
a.lib-chip.active,
[data-theme="dark"] a.lib-chip.active:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    background: var(--primary);
    border-color: var(--primary);
    color: #fff;
}
.lib-chip-count { color: var(--text-muted); font-size: var(--text-xs); }
.lib-chip.active .lib-chip-count { color: rgba(255, 255, 255, 0.8); }

.lib-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 330px), 1fr));
    gap: 0.9rem;
}
.lib-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
}
.lib-card h3 {
    margin: 0;
    font-size: var(--text-body);
    line-height: 1.35;
}
.lib-card h3 a,
[data-theme="dark"] .lib-card h3 a:not([role="button"]):not(.ghost):not(.topbar-icon-btn):not(.topbar-dropdown-item) {
    color: var(--heading);
    text-decoration: none;
}
.lib-card h3 a:hover { color: var(--primary); }
.lib-card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    align-items: center;
}
.lib-card-tags {
    color: var(--text-muted);
    font-size: var(--text-xs);
    margin-top: auto;
}

.lib-article-header h1 { margin: 0; font-size: var(--step-4); }
.lib-article-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin: 0.5rem 0 1rem;
}

.lib-cta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1.25rem;
    margin-top: 1.25rem;
    padding: 1rem 1.25rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-left: 4px solid var(--primary);
    border-radius: var(--radius);
}
.lib-cta h2 { margin: 0 0 0.25rem; font-size: var(--step-2); }
.lib-cta p { margin: 0; color: var(--text-muted); font-size: var(--text-sm); max-width: 72ch; }
.lib-cta-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }

.lib-related-heading { font-size: var(--step-2); margin: 0 0 0.75rem; }

@media (width <= 640px) {
    .lib-cta { flex-direction: column; align-items: flex-start; }
    .lib-shell { padding: 1.25rem 0.85rem 3rem; }
}

/* ── Report Builder ──
   Builder-page pieces: a single compact composer card (name/entity row plus
   a wrapping toolbar; Columns popover pill, Add Filter pill, inline sort)
   above a full-width live preview, column-picker checkbox grids grouped per
   entity behind native <details> inside the popover (summaries use the
   global pill treatment), and the filter rows (field / condition / value /
   remove). Everything else is stock cards, section-headers, and form-grid. */
.rb-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem 1rem;
    margin-top: 0.9rem;
}
.rb-toolbar label {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0;
    font-size: var(--text-sm);
    white-space: nowrap;
}
.rb-toolbar select { width: auto; margin: 0; }
.rb-filters { margin-top: 0.6rem; }

.rb-col-groups { display: flex; flex-direction: column; gap: 0.5rem; }
.rb-col-group { width: 100%; } /* summary stays a content-sized pill (global inline-flex) */
.rb-col-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr));
    gap: 0.1rem 0.9rem;
    margin-top: 0.35rem;
    padding-left: 0.5rem;
    width: 100%;
}
.rb-col-option {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0;
    padding: 0.1rem 0;
    font-size: var(--text-sm);
    color: var(--text);
    cursor: pointer;
}
.rb-col-option input[type="checkbox"] { width: auto; margin: 0; flex: 0 0 auto; }

.rb-filter-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.rb-filter-row select,
.rb-filter-row input:not([type="hidden"]) { width: auto; flex: 1 1 150px; margin: 0; }
.rb-filter-row .btn-icon { flex: 0 0 auto; }

/* ── Scheduled Delivery (Settings tab) ──
   Saved-report checkbox list inside the add/edit recipient forms. Same
   checkbox-row treatment as .rb-col-option, in a quiet bordered fieldset. */
.subscription-report-list {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.9rem 0.75rem;
    margin: 0.9rem 0 0;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    max-height: 220px;
    overflow-y: auto;
}
.subscription-report-list legend { padding: 0 0.35rem; }
.subscription-report-list label {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0;
    padding: 0.1rem 0;
    font-size: var(--text-sm);
    cursor: pointer;
}
.subscription-report-list input[type="checkbox"] { width: auto; margin: 0; flex: 0 0 auto; }

/* ── Metrics health lights (SystemAdmin /system/metrics) ── */
.health-light { display: inline-flex; align-items: center; gap: 0.35rem; font-weight: 600; font-size: var(--text-sm); text-transform: capitalize; }
.health-light::before { content: ""; width: 0.6rem; height: 0.6rem; border-radius: 50%; background: var(--text-muted); flex: 0 0 auto; }
.health-light.green::before  { background: var(--success); }
.health-light.yellow::before { background: var(--warning); }
.health-light.red::before    { background: var(--danger); }
.health-light.blue::before   { background: var(--info); }
.trend-arrow { font-size: var(--text-sm); color: var(--text-muted); }
.trend-arrow.improving  { color: var(--success); }
.trend-arrow.declining  { color: var(--danger); }

/* Per-tenant Signals panel: plain-language insight rows, icon tinted by tone. */
.signal-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.signal { display: flex; align-items: flex-start; gap: 0.6rem; font-size: var(--text-sm); color: var(--text); }
.signal > i { flex: 0 0 auto; margin-top: 0.1rem; color: var(--text-muted); }
.signal--success > i { color: var(--success); }
.signal--warning > i { color: var(--warning); }
.signal--danger  > i { color: var(--danger); }
.signal--info    > i { color: var(--info); }
