:root {
    /* Pulse shared semantic palette. Keep component accents and states aligned. */
    color-scheme: light;
    --pulse-font-sans: 'Segoe UI Variable Text', 'Aptos', Inter, 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
    --pulse-canvas: #f5f7fa;
    --pulse-navy-950: #071827;
    --pulse-navy-900: #0f172a;
    --pulse-navy-800: #172033;
    --pulse-surface: #fff;
    --pulse-surface-soft: #f8fafc;
    /* Surfaces are semantic, not a synonym for "dark operator panel". A page
       can now add depth without hard-coding a second palette that stays navy
       when the application is in light mode. */
    --pulse-surface-raised: var(--pulse-surface);
    --pulse-surface-inset: #eef3f8;
    --pulse-surface-overlay: rgba(255, 255, 255, .98);
    --pulse-border: #dbe2ea;
    --pulse-border-strong: #cbd5e1;
    --pulse-text: #0f172a;
    --pulse-muted: #59697d;
    --pulse-placeholder: #526477;
    --pulse-action: #0f766e;
    --pulse-action-hover: #115e59;
    --pulse-on-action: #fff;
    --pulse-accent: #0f766e;
    --pulse-accent-bright: #2dd4bf;
    --pulse-accent-soft: #ecfeff;
    --pulse-accent-border: #ccfbf1;
    --pulse-link: #0369a1;
    --pulse-info: #0ea5e9;
    --pulse-info-strong: #0369a1;
    --pulse-info-bright: #44a7ff;
    --pulse-info-soft: #e0f2fe;
    --pulse-info-border: #bae6fd;
    --pulse-blue: #2563eb;
    --pulse-blue-bright: #60a5fa;
    --pulse-success: #16a34a;
    --pulse-success-bright: #4ade80;
    --pulse-success-text: #047857;
    --pulse-success-soft: #ecfdf5;
    --pulse-success-border: #a7f3d0;
    --pulse-warning: #d97706;
    --pulse-warning-bright: #fbbf24;
    --pulse-warning-text: #9a3412;
    --pulse-warning-soft: #fff7ed;
    --pulse-warning-border: #fed7aa;
    --pulse-danger: #dc2626;
    --pulse-danger-bright: #f87171;
    --pulse-danger-text: #b91c1c;
    --pulse-danger-soft: #fee2e2;
    --pulse-danger-border: #fecaca;
    /* Solid, not a 35% wash. Every screen in the app draws its focus outline from
       this one token, and at that alpha the ring measured about 1.4:1 against a
       white card and 1.9:1 against the dark canvas - a keyboard operator could not
       see where they were. */
    --pulse-focus-ring: #0284c7;
    /* Outlines take the solid colour above; box-shadow halos (Bootstrap's focus
       ring, .btn:focus below) are glows drawn around the control and need alpha,
       or the halo becomes a second solid band on top of the outline. */
    --pulse-focus-ring-soft: rgba(2, 132, 199, .35);
    --pulse-radius-sm: 8px;
    --pulse-radius-md: 12px;
    --pulse-radius-lg: 14px;
    --pulse-radius-xl: 16px;
    --pulse-shadow-sm: 0 1px 3px rgba(15, 23, 42, .04);
    --pulse-shadow-md: 0 4px 16px rgba(15, 23, 42, .06);
    --pulse-shadow-lg: 0 16px 40px rgba(15, 23, 42, .12);
    --pulse-ease-out: cubic-bezier(.22, 1, .36, 1);
    --pulse-motion-fast: 140ms;
    --pulse-motion-normal: 180ms;
    --pulse-disabled-opacity: .58;
    --pulse-disabled-text: var(--pulse-muted);
    --pulse-disabled-border: var(--pulse-border);
    --pulse-disabled-surface: var(--pulse-surface-soft);
    --pulse-page-gutter: clamp(12px, 1vw, 20px);
    /* One responsive geometry contract for full-page workspaces. Feature pages
       may keep their own accent, but not invent a new title scale, hero height,
       control height, or outer rhythm. */
    --pulse-workspace-max: 100%;
    --pulse-workspace-gap: clamp(12px, 1.1vw, 16px);
    --pulse-workspace-pad-x: var(--pulse-page-gutter);
    /* Flat, not a clamp: the 3-6px a wide viewport used to add landed above the
       page title, pushing the content the operator came for further down. */
    --pulse-workspace-pad-top: 12px;
    --pulse-workspace-pad-bottom: clamp(24px, 2.5vw, 36px);
    --pulse-workspace-hero-pad-y: 14px;
    --pulse-workspace-hero-pad-x: clamp(16px, 1.5vw, 20px);
    --pulse-workspace-title-size: clamp(1.25rem, 1.6vw, 1.5rem);
    --pulse-workspace-lede-size: .82rem;
    --pulse-workspace-eyebrow-size: .75rem;
    --pulse-commandbar-height: 64px;
    --pulse-sticky-top: calc(var(--pulse-commandbar-height) + 6px);
    --pulse-control-min-height: 44px;
    --pulse-control-compact-height: 38px;
    --pulse-control-surface: var(--pulse-surface-raised);
    --pulse-control-surface-hover: var(--pulse-surface-soft);
    --pulse-control-border: var(--pulse-border);
    --pulse-control-text: var(--pulse-text);

    /* Compatibility aliases for four names that reached page CSS before they
       reached this file - and were then never defined anywhere, in either
       project. Every consumer therefore fell through to its hard-coded light
       fallback and stayed light in dark theme: Reports' recovery panel and its
       paused pill are a near-white slab on the #0b1220 page, the operator
       preference panels' hint text sits on a static grey, and the severity-rule
       breadcrumb keeps a light-theme blue. Each alias points at the canonical
       token, which does flip, so those surfaces read correctly in both themes
       from here - and because the alias resolves through the canonical token, a
       BrandPack palette reaches them too.

       These are a bridge, not a vocabulary. New CSS uses the canonical name on
       the right; WorkspaceHeroConsistencyContractTests already forbids the two
       -muted names in the workspace pages, and the pages listed above should be
       migrated off them so this block can be deleted. */
    --pulse-surface-muted: var(--pulse-surface-soft);
    --pulse-text-muted: var(--pulse-muted);
    --pulse-on-accent: var(--pulse-on-action);
    --pulse-primary: var(--pulse-link);

    --bs-body-font-family: var(--pulse-font-sans);
    --bs-body-color: var(--pulse-text);
    --bs-body-bg: var(--pulse-canvas);
    --bs-border-color: var(--pulse-border);
    --bs-primary: #0f766e;
    --bs-primary-rgb: 15, 118, 110;
    --bs-link-color: var(--pulse-link);
    --bs-focus-ring-color: var(--pulse-focus-ring-soft);
}

html[data-theme="dark"] {
    color-scheme: dark;
    --pulse-canvas: #0b1220;
    --pulse-surface: #111a2e;
    --pulse-surface-soft: #16213a;
    --pulse-surface-inset: #0d1728;
    --pulse-surface-overlay: rgba(9, 20, 34, .98);
    --pulse-border: #233047;
    --pulse-border-strong: #334155;
    --pulse-text: #e2e8f0;
    --pulse-muted: #94a3b8;
    --pulse-placeholder: #8da0b5;
    --pulse-accent: #5eead4;
    --pulse-accent-soft: #0e2a3a;
    --pulse-accent-border: #164e63;
    --pulse-link: #7dd3fc;
    --pulse-info: #38bdf8;
    --pulse-info-strong: #7dd3fc;
    --pulse-info-soft: #0e2a3a;
    --pulse-info-border: #164e63;
    --pulse-success-text: #86efac;
    --pulse-success-soft: #123627;
    --pulse-success-border: #275b47;
    --pulse-warning-text: #fbbf24;
    --pulse-warning-soft: #392f17;
    --pulse-warning-border: #5f5129;
    --pulse-danger-text: #fca5a5;
    --pulse-danger-soft: #3f1726;
    --pulse-danger-border: #6a3540;
    /* The dark theme never restated the focus colour, so the ring stayed a mid
       blue on a near-black page. Bright enough to find at two metres. */
    --pulse-focus-ring: #38bdf8;
    --pulse-focus-ring-soft: rgba(56, 189, 248, .4);
    --pulse-disabled-opacity: .64;
    /* Dark surfaces keep the same restrained elevation geometry. */
    --pulse-shadow-sm: 0 1px 3px rgba(2, 6, 17, .2);
    --pulse-shadow-md: 0 4px 16px rgba(2, 6, 17, .28);
    --pulse-shadow-lg: 0 16px 40px rgba(2, 6, 17, .4);
    /* Hover has to move toward the light on a dark page. The light-theme hover
       darkens the action colour, which on this canvas reads as the button
       going dead rather than responding; this one lifts it instead and still
       measures about 4.8:1 against --pulse-on-action. */
    --pulse-action-hover: #118079;
}

/* The bar is 64px from the desktop down, so this only restates the token below
   the sidebar breakpoint. It stays stated here because --pulse-sticky-top is
   derived from it and five workspace pages park their sticky editor at that
   offset - when only the bar shrank, those panels floated below it. */
@media (max-width: 820px) {
    :root {
        --pulse-commandbar-height: 64px;
    }
}

/* Phones wrap the command bar into controls plus a full-width search row.
   Keeping the desktop/tablet token here made every sticky surface believe the
   bar ended halfway through that second row. */
@media (max-width: 560px) {
    :root {
        --pulse-commandbar-height: 132px;
    }
}

html, body {
    background: var(--pulse-canvas);
    color: var(--pulse-text);
    font-family: var(--pulse-font-sans);
    margin: 0;
}

html {
    min-height: 100%;
    scrollbar-color: var(--pulse-border-strong) transparent;
    /* The gutter stays reserved when body.command-open / .nav-open hides the
       overflow, so opening the palette or the drawer does not shift the shell
       sideways by the width of the scrollbar. */
    scrollbar-gutter: stable;
    scrollbar-width: thin;
}

/* A video in the browser's fullscreen layer uses the entire screen. */
html:has(.live-stage:fullscreen) {
    scrollbar-gutter: auto;
}

body {
    min-height: 100vh;
    min-height: 100dvh;
    -webkit-font-smoothing: antialiased;
}

::selection {
    background: rgba(45, 212, 191, .24);
    color: var(--pulse-text);
}

button,
input,
select,
textarea {
    color: inherit;
    font: inherit;
}

:where(.pulse-content input, .pulse-content textarea)::placeholder {
    color: var(--pulse-placeholder);
    opacity: 1;
}

:where(button, summary, [role="button"]):not(:disabled):not([aria-disabled="true"]) {
    cursor: pointer;
}

:where(button, input, select, textarea):disabled,
:where([aria-disabled="true"]) {
    cursor: not-allowed;
    opacity: var(--pulse-disabled-opacity);
}

:where(input[type="checkbox"], input[type="radio"], input[type="range"], progress) {
    accent-color: var(--pulse-action);
}

@media (any-pointer: coarse) {
    /* Coarse-pointer devices need the same physical target size everywhere,
       including settings pages that were authored before the mobile shell. */
    button,
    select,
    textarea,
    input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) {
        min-height: var(--pulse-control-min-height);
    }
}

:where(a, button, input, select, textarea, summary, [contenteditable="true"], [tabindex]):focus-visible {
    outline: 3px solid var(--pulse-focus-ring);
    outline-offset: 2px;
}

body.nav-open,
body.command-open {
    overflow: hidden;
}

a, .btn-link {
    color: var(--pulse-link);
    text-underline-offset: .18em;
}

.btn-primary {
    --bs-btn-bg: var(--pulse-action);
    --bs-btn-border-color: var(--pulse-action);
    --bs-btn-hover-bg: var(--pulse-action-hover);
    --bs-btn-hover-border-color: var(--pulse-action-hover);
    --bs-btn-active-bg: var(--pulse-action-hover);
    --bs-btn-active-border-color: var(--pulse-action-hover);
    color: var(--pulse-on-action);
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 4px var(--pulse-focus-ring-soft);
}

.content {
    padding-top: 1.1rem;
}

/* ----------------------------------------------------------------------- */
/* RETIRED: the pre-shell top header.                                       */
/*                                                                          */
/* No markup anywhere in the solution renders .relay-header, .relay-nav,     */
/* .relay-search, .relay-status, .relay-header-tools, .relay-brand,          */
/* .brand-mark, .alarms-nav or .dashboard-link - the shell in MainLayout     */
/* replaced all of them. Do not extend this block or copy from it: it is a   */
/* second, dark-chrome palette for controls the app now paints from the      */
/* tokens above, and .theme-toggle here still carries the old white-on-navy  */
/* treatment that would surface on any new markup reusing that class name    */
/* without its own rule (MainLayout's scoped rule is what covers the live    */
/* one). Deleting the block needs a visual pass across the account and error */
/* layouts, so it is left in place rather than removed blind.                */
/* ----------------------------------------------------------------------- */
.relay-header {
    align-items: center;
    backdrop-filter: blur(18px);
    background: linear-gradient(135deg, #071827 0%, #0b3850 54%, #0f766e 100%);
    box-shadow: 0 18px 45px rgba(7, 24, 39, 0.18);
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    min-height: 68px;
    padding: 10px 28px;
    position: sticky;
    top: 0;
    z-index: 20;
}

.relay-header nav,
.relay-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: flex-end;
    align-items: center;
}

.relay-search {
    align-items: center;
    background: rgba(255, 255, 255, .12);
    border: 1px solid rgba(255, 255, 255, .2);
    border-radius: 999px;
    display: flex;
    flex: 1 1 220px;
    gap: 8px;
    max-width: 420px;
    min-width: 150px;
    padding: 6px 14px;
}

.relay-search-icon {
    font-size: 13px;
    opacity: .8;
}

.relay-search input {
    background: transparent;
    border: 0;
    color: #f9fafb;
    flex: 1;
    font-size: 14px;
    outline: none;
    min-width: 0;
}

.relay-search input::placeholder {
    color: rgba(249, 250, 251, .65);
}

.relay-header-tools {
    align-items: center;
    display: flex;
    gap: 10px;
}

.relay-status {
    align-items: center;
    color: #e5f2f1;
    display: inline-flex;
    font-size: 12px;
    font-weight: 600;
    gap: 6px;
    white-space: nowrap;
}

.status-dot {
    background: #9ca3af;
    border-radius: 50%;
    box-shadow: 0 0 0 3px rgba(156, 163, 175, .25);
    height: 9px;
    width: 9px;
}

.relay-status.is-online .status-dot {
    background: #22c55e;
    box-shadow: 0 0 0 3px rgba(34, 197, 94, .25);
}

.relay-status.is-offline .status-dot {
    background: #ef4444;
    box-shadow: 0 0 0 3px rgba(239, 68, 68, .25);
}

.theme-toggle {
    background: rgba(255, 255, 255, .14);
    border: 1px solid rgba(255, 255, 255, .22);
    border-radius: 999px;
    color: #fff;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 7px 11px;
    transition: background .18s ease, transform .18s ease;
}

.theme-toggle:hover {
    background: rgba(255, 255, 255, .26);
    transform: translateY(-1px);
}

.alarms-nav {
    align-items: center;
    display: inline-flex;
    gap: 6px;
}

.alarm-badge {
    background: #b91c1c;
    border-radius: 999px;
    color: #fff;
    font-size: 12px;
    font-weight: 800;
    line-height: 1;
    min-width: 18px;
    padding: 3px 6px;
    text-align: center;
}

.relay-header a {
    color: #f9fafb;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: .01em;
    opacity: .92;
    padding: 9px 12px;
    border-radius: 999px;
    text-decoration: none;
    transition: background .18s ease, opacity .18s ease, transform .18s ease;
}

.relay-brand {
    align-items: center;
    display: inline-flex;
    font-size: 17px !important;
    font-weight: 800 !important;
    gap: 10px;
    padding-left: 0 !important;
    white-space: nowrap;
}

.brand-mark {
    align-items: center;
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .28);
    border-radius: 12px;
    display: inline-flex;
    height: 34px;
    justify-content: center;
    width: 34px;
}

.relay-header a:hover {
    background: rgba(255, 255, 255, .14);
    opacity: 1;
    text-decoration: none;
    transform: translateY(-1px);
}

.relay-header a.nav-item.active {
    background: rgba(255, 255, 255, .22);
    opacity: 1;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .25);
}

.breadcrumbs {
    align-items: center;
    color: var(--pulse-muted);
    display: flex;
    flex-wrap: wrap;
    font-size: .875rem;
    gap: 6px;
    margin-bottom: 10px;
}

.breadcrumbs a {
    color: var(--pulse-link);
    font-weight: 600;
    text-decoration: none;
}

.breadcrumbs a:hover {
    text-decoration: underline;
}

.breadcrumbs .sep {
    color: var(--pulse-border-strong);
}

.breadcrumbs .current {
    /* The camera detail and building layout pages use this breadcrumb with no
       colours of their own, so a fixed near-black left the name of the page you
       are standing on unreadable on the dark canvas. */
    color: var(--pulse-text);
    font-weight: 700;
}

.relay-header .dashboard-link {
    background: #14b8a6;
    color: #052e2b;
    font-weight: 800;
}

@media (max-width: 760px) {
    .relay-header {
        align-items: flex-start;
        flex-direction: column;
        gap: 10px;
        min-height: auto;
        padding: 14px 18px;
        position: static;
    }

    .relay-search {
        box-sizing: border-box;
        flex: 0 0 auto;
        max-width: none;
        width: 100%;
    }

    .relay-header nav,
    .relay-nav {
        gap: 4px;
        justify-content: flex-start;
        width: 100%;
    }

    .relay-header a.nav-item {
        font-size: 14px;
        padding: 7px 9px;
    }

    .relay-header-tools {
        justify-content: space-between;
        width: 100%;
    }
}

h1:focus {
    outline: none;
}

/* The only two places in the app that stated "accepted" and "rejected" in a
   palette of their own: a flat #26b050 and #e50000 that matched no chip, banner
   or badge on the same form, and that were never restated for dark theme. The
   tokens are the same green and red every other state cue uses, and both clear
   3:1 against either canvas. */
.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--pulse-success);
}

.invalid {
    outline: 1px solid var(--pulse-danger);
}

/* A field keeps .valid.modified once it has been typed in, and that selector is
   more specific than the shared focus rule near the top of this file - so the
   focus ring vanished from every field the moment it was edited, and a keyboard
   user filling in a form lost track of where they were. */
.valid.modified:not([type=checkbox]):focus-visible,
.invalid:focus-visible {
    outline: 3px solid var(--pulse-focus-ring);
    outline-offset: 2px;
}

.validation-message {
    /* The message a user has to read before a form will save. The flat red sat at
       under 4:1 on the dark canvas; the token is legible in both themes. */
    color: var(--pulse-danger-text);
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ----------------------------------------------------------------------- */
/* Dark theme (toggled via [data-theme="dark"] on <html> by pulse-ui.js)   */
/* ----------------------------------------------------------------------- */
/* These two rules restate what the token block above already says. They are kept
   only because they outrank a page rule that a component might set on html/body,
   but they must read from the tokens: the literals they used to carry were a
   second, slightly different dark palette (#e5e7eb ink instead of --pulse-text,
   and a link colour that disagreed with --pulse-link, so a breadcrumb link and
   an ordinary link were two different colours on the same dark screen). */
html[data-theme="dark"],
html[data-theme="dark"] body {
    background: var(--pulse-canvas);
    color: var(--pulse-text);
}

html[data-theme="dark"] a,
html[data-theme="dark"] .btn-link {
    color: var(--pulse-link);
}

html[data-theme="dark"] h1,
html[data-theme="dark"] h2 {
    color: var(--pulse-text);
}

/* Generic light surfaces used across pages. These rescue camera surfaces that
   still paint a literal white; they carry !important, so they have to be written
   in tokens or they win against - and silently discard - a BrandPack palette and
   any future change to the dark surface ramp. The literals they replaced were
   already the dark token values, except the ink, which was a dimmer #cbd5e1 than
   the --pulse-text every other panel uses. */
html[data-theme="dark"] .camera-header,
html[data-theme="dark"] .camera-card,
html[data-theme="dark"] .camera-loading,
html[data-theme="dark"] .camera-error,
html[data-theme="dark"] .camera-empty,
html[data-theme="dark"] .live-message,
html[data-theme="dark"] .camera-alarms-panel,
html[data-theme="dark"] .detail-alarm-table {
    background: var(--pulse-surface) !important;
    border-color: var(--pulse-border) !important;
    box-shadow: none !important;
    color: var(--pulse-text);
}

html[data-theme="dark"] .camera-name {
    color: var(--pulse-text);
}

html[data-theme="dark"] .detail-alarm-table th,
html[data-theme="dark"] .panel-title-row {
    background: var(--pulse-surface-soft) !important;
    color: var(--pulse-text) !important;
    border-color: var(--pulse-border) !important;
}

html[data-theme="dark"] .detail-alarm-table td {
    border-color: var(--pulse-border) !important;
}

html[data-theme="dark"] .camera-card:hover {
    background: var(--pulse-surface-soft) !important;
}

html[data-theme="dark"] .camera-count {
    background: var(--pulse-info-soft);
    color: var(--pulse-info-strong);
}

html[data-theme="dark"] .camera-icon {
    background: var(--pulse-info-soft);
}

@media (prefers-reduced-motion: reduce) {
    html:focus-within {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
        transition-duration: .01ms !important;
    }
}

/* ----------------------------------------------------------------------- */
/* Live alarm toasts (the shell moves this one stack into fullscreen roots) */
/* ----------------------------------------------------------------------- */
#relay-toast-stack,
.relay-alarm-fullscreen-portal {
    align-items: flex-end;
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Four retained toasts with long camera and server names need more height
       than a zoomed laptop has, and the oldest ones fell off the bottom of the
       screen where their dismiss button could not be reached. */
    max-height: calc(100dvh - 90px);
    overflow-y: auto;
    overscroll-behavior: contain;
    pointer-events: none;
    position: fixed;
    right: 16px;
    top: 74px;
    width: min(348px, calc(100vw - 24px));
    z-index: 1080;
}

#relay-toast-stack > *,
.relay-alarm-fullscreen-portal > * {
    pointer-events: auto;
}

.relay-alarm-fullscreen-portal {
    max-height: calc(100dvh - 32px);
    right: max(16px, env(safe-area-inset-right));
    top: max(16px, env(safe-area-inset-top));
}

.relay-toast-sound {
    align-items: center;
    background: var(--pulse-surface);
    border: 1px solid var(--pulse-border);
    border-radius: 999px;
    box-shadow: var(--pulse-shadow-sm);
    color: var(--pulse-muted);
    cursor: pointer;
    display: inline-flex;
    height: 34px;
    justify-content: center;
    width: 34px;
}

.relay-toast-sound[hidden] {
    display: none;
}

.relay-toast-sound:hover {
    border-color: var(--pulse-danger);
    color: var(--pulse-danger-text);
}

.relay-toast-sound svg {
    fill: none;
    height: 18px;
    stroke: currentColor;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-width: 1.7;
    width: 18px;
}

.relay-toast-sound .relay-toast-sound-on,
.relay-toast-sound.is-sound-on .relay-toast-sound-off {
    display: none;
}

.relay-toast-sound.is-sound-on .relay-toast-sound-on {
    display: block;
}

.relay-toast-sound.is-sound-on {
    border-color: var(--pulse-danger-border);
    color: var(--pulse-danger-text);
}

.relay-toast {
    animation: relay-toast-pulse 1.4s ease-in-out 4;
    background: var(--pulse-surface);
    border: 1px solid var(--pulse-danger);
    border-radius: var(--pulse-radius-md);
    box-shadow: 0 0 0 1px rgba(239, 68, 68, .18), var(--pulse-shadow-md);
    display: flex;
    overflow: hidden;
    transition: opacity .18s ease, transform .18s ease;
    width: 100%;
}

.relay-toast.is-leaving {
    opacity: 0;
    transform: translateX(14px);
}

@keyframes relay-toast-pulse {
    0%, 100% {
        box-shadow: 0 0 0 1px rgba(239, 68, 68, .18), var(--pulse-shadow-md);
    }

    50% {
        box-shadow: 0 0 0 6px rgba(239, 68, 68, .3), var(--pulse-shadow-md);
    }
}

.relay-toast-link {
    align-items: center;
    color: inherit;
    display: flex;
    flex: 1;
    gap: 10px;
    min-width: 0;
    padding: 10px 4px 10px 10px;
    text-decoration: none;
}

.relay-toast-link:hover {
    text-decoration: none;
}

.relay-toast-thumb {
    background: var(--pulse-navy-900);
    border-radius: 8px;
    flex: 0 0 auto;
    height: 44px;
    object-fit: cover;
    width: 66px;
}

.relay-toast-copy {
    display: grid;
    gap: 2px;
    min-width: 0;
}

/* One line inside a 348px toast is about 28 characters, and the meta line carries
   "camera name - server name": the two things that say where the alarm is were the
   first to be cut off. Two lines hold a real installation's names, and only past
   that does it ellipsize. */
.relay-toast-title,
.relay-toast-meta {
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    display: -webkit-box;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
}

.relay-toast-title {
    color: var(--pulse-text);
    font-size: 14px;
    font-weight: 800;
}

.relay-toast-meta {
    color: var(--pulse-muted);
    font-size: 12px;
    font-weight: 600;
}

.relay-toast-time {
    color: var(--pulse-danger-text);
    font-size: 12px;
    font-weight: 800;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.relay-toast-close {
    align-self: flex-start;
    background: transparent;
    border: 0;
    color: var(--pulse-muted);
    cursor: pointer;
    flex: 0 0 auto;
    font-size: 16px;
    line-height: 1;
    padding: 8px 10px;
}

.relay-toast-close:hover {
    color: var(--pulse-danger-text);
}

html[data-theme="dark"] .relay-toast {
    border-color: #7f2d3a;
}

html[data-theme="dark"] .relay-toast-thumb {
    background: #060b14;
}

/* Stable source identity shown beside camera names across server and WASM views. */
.server-id-badge {
    align-items: center;
    background: var(--pulse-accent-soft);
    border: 1px solid var(--pulse-accent-border);
    border-radius: 999px;
    color: var(--pulse-accent);
    display: inline-flex;
    font-size: .75rem;
    font-weight: 850;
    letter-spacing: .035em;
    line-height: 1;
    margin-inline-start: .38rem;
    max-width: 10rem;
    min-height: 1.25rem;
    overflow: hidden;
    padding: .18rem .42rem;
    text-overflow: ellipsis;
    vertical-align: .14em;
    white-space: nowrap;
}

html[data-theme="dark"] .server-id-badge {
    background: color-mix(in srgb, var(--pulse-accent) 14%, transparent);
}

html.pulse-is-navigating::before {
    content: '';
    position: fixed;
    z-index: 2147483647;
    inset: 0 auto auto 0;
    width: 38%;
    height: 3px;
    pointer-events: none;
    background: linear-gradient(90deg, var(--pulse-accent), var(--pulse-info-bright));
    box-shadow: 0 0 14px rgba(14, 165, 233, .5);
    animation: pulse-navigation-progress .8s var(--pulse-ease-out) infinite alternate;
}

@keyframes pulse-navigation-progress {
    from { transform: translateX(-35%); opacity: .72; }
    to { transform: translateX(200%); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    html.pulse-is-navigating::before {
        width: 100%;
        animation: none;
    }
}

/* The command bar wraps to two rows on a phone. Notifications start below the
   complete bar rather than covering the search shortcut in its second row. */
@media (max-width: 560px) {
    #relay-toast-stack {
        max-height: calc(100dvh - var(--pulse-commandbar-height) - 16px - env(safe-area-inset-top));
        top: calc(var(--pulse-commandbar-height) + env(safe-area-inset-top));
    }
}

@media (prefers-reduced-motion: reduce) {
    .relay-toast {
        /* Steady red ring instead of the pulse. */
        animation: none;
        box-shadow: 0 0 0 4px rgba(239, 68, 68, .28), var(--pulse-shadow-md);
    }
}

/* Shared basemap chrome. The map data is rendered as a single WebGL canvas,
   avoiding the raster checkerboard that appeared while dozens of independent
   image tiles arrived. Light and dark are separate authored styles; no colour
   inversion is applied to geography, labels, markers, or controls. */
.pulse-map-canvas {
    background:
        radial-gradient(circle at 18% 22%, rgba(14, 165, 233, .08), transparent 28%),
        linear-gradient(135deg, #edf3f6, #dfe9ee);
}

.pulse-map-canvas .leaflet-gl-layer,
.pulse-map-canvas .maplibregl-map {
    background: transparent;
}

.pulse-map-canvas .maplibregl-canvas {
    image-rendering: auto;
    outline: 0;
}

/* Attribution links are real, focusable controls, and the libraries render
   them as bare inline anchors about 10 CSS pixels tall. That is below the
   24x24 minimum target size every other control on these pages meets, so a
   thumb cannot reliably hit "Leaflet" or "OpenStreetMap contributors".
   Give each link a 24px hit area without changing the text size; the box
   grows, the glyphs do not. */
.leaflet-container .leaflet-control-attribution a,
.leaflet-container .maplibregl-ctrl-attrib a {
    display: inline-flex;
    align-items: center;
    min-width: 24px;
    min-height: 24px;
    padding: 0 0.25rem;
}

.pulse-map-canvas.is-basemap-loading::after {
    position: absolute;
    z-index: 350;
    top: 12px;
    left: 50%;
    padding: 7px 11px 7px 28px;
    border: 1px solid rgba(148, 163, 184, .45);
    border-radius: 999px;
    color: #334155;
    background:
        radial-gradient(circle at 15px 50%, #0f827d 0 4px, transparent 5px),
        rgba(255, 255, 255, .93);
    box-shadow: 0 7px 20px rgba(15, 23, 42, .15);
    content: "Preparing map";
    font-size: 12px;
    font-weight: 800;
    pointer-events: none;
    transform: translateX(-50%);
}

html[data-theme="dark"] .pulse-map-canvas {
    background:
        radial-gradient(circle at 18% 22%, rgba(14, 165, 233, .1), transparent 28%),
        linear-gradient(135deg, #111b2b, #162235);
}

html[data-theme="dark"] .pulse-map-canvas.is-basemap-loading::after {
    border-color: rgba(100, 116, 139, .58);
    color: #dbe7f2;
    background:
        radial-gradient(circle at 15px 50%, #2dd4bf 0 4px, transparent 5px),
        rgba(15, 23, 42, .94);
}

@media (max-width: 620px) {
    .pulse-map-canvas.is-basemap-loading::after {
        top: 58px;
    }
}


/* Leaflet builds marker elements outside the component's scoped DOM, so these
   cannot live in Buildings.razor.css - scoped attributes never reach them. */
.building-pin {
    align-items: center;
    background: #334155;
    border: 2px solid #fff;
    border-radius: 9px;
    box-shadow: 0 0 0 3px rgba(51, 65, 85, .25), 0 3px 9px rgba(5, 18, 32, .38);
    color: #fff;
    display: flex;
    justify-content: center;
}

.building-pin::after {
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 7px solid #334155;
    bottom: -7px;
    content: "";
    left: 10px;
    position: absolute;
}

.building-pin > span {
    align-items: center;
    display: flex;
    justify-content: center;
}

.building-pin svg {
    height: 21px;
    width: 21px;
}

.building-pin.is-selected {
    background: #f59e0b;
    box-shadow: 0 0 0 4px rgba(245, 158, 11, .3), 0 2px 8px rgba(5, 18, 32, .4);
}

.building-pin.is-selected::after {
    border-top-color: #f59e0b;
}

/* The not-yet-saved building being placed: dashed, so it cannot be mistaken
   for a stored one. */
.building-pin.is-draft {
    background: #d97706;
    border-style: dashed;
    box-shadow: 0 0 0 4px rgba(217, 119, 6, .28), 0 2px 8px rgba(5, 18, 32, .4);
    cursor: grab;
}

.building-pin.is-draft::after {
    border-top-color: #d97706;
}

#buildings-map.is-placing,
#buildings-map.is-placing .leaflet-grab,
#buildings-map.is-placing .leaflet-interactive {
    cursor: crosshair !important;
}

/* ---- a camera on a building or floor drawing ------------------------------
   The same mark the operations map uses: same symbol, same blue, same red ring
   while an alarm is live. Global rather than scoped because both the settings
   page and the building layout render CameraPlanPin, and they live in
   different projects. */
.camera-plan-pin {
    position: absolute;
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: 3px 7px 3px 3px;
    border: 2px solid #fff;
    border-radius: 999px;
    color: #fff;
    background: #2563eb;
    box-shadow: 0 5px 15px rgba(15, 23, 42, .32);
    font-size: 12px;
    font-weight: 800;
    line-height: 1.2;
    white-space: nowrap;
    /* Anchored on the camera symbol, not on the middle of the pill. The label
       grows to the right, so centring the whole chip would put the spot
       somewhere inside the camera's own name - and the cone would then leave
       from a point the mark is not standing on. 2px border + 3px padding +
       half of the 18px mark. The inverse scale the plan viewport publishes keeps
       the chip a usable on-screen size when a large drawing is fitted down; the
       origin is the anchor, so the mark stays on the surveyed spot. */
    transform: translate(-14px, -50%) scale(var(--plan-inverse-scale, 1));
    transform-origin: 14px 50%;
}

button.camera-plan-pin.is-interactive {
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;
}

button.camera-plan-pin.is-interactive:focus-visible {
    outline: 3px solid var(--pulse-focus-ring);
    outline-offset: 3px;
}

button.camera-plan-pin.is-interactive:hover {
    filter: brightness(1.08);
}

@media (any-pointer: coarse) {
    .relay-header a,
    .theme-toggle,
    .alarm-actions a,
    .alarm-row-links a,
    .alarm-detail-button,
    .relay-toast-sound,
    .relay-toast-close {
        min-height: var(--pulse-control-min-height);
    }

    .theme-toggle,
    .relay-toast-sound,
    .relay-toast-close {
        min-width: var(--pulse-control-min-height);
    }

    button.camera-plan-pin.is-interactive::before {
        position: absolute;
        inset: -8px;
        content: "";
    }
}

.camera-plan-pin-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
}

.camera-plan-pin-mark svg {
    display: block;
    width: 15px;
    height: 15px;
}

.camera-plan-pin-label {
    overflow: hidden;
    max-width: 9rem;
    text-overflow: ellipsis;
}

/* No label: the pill collapses to the round camera symbol the map uses. The
   name is in the tooltip, where the map keeps it too. */
.camera-plan-pin:not(:has(.camera-plan-pin-label)) {
    padding: 3px;
    border-radius: 50%;
}

/* An open alarm turns the camera red, exactly as it does on the map. */
.camera-plan-pin.is-active {
    background: #dc2626;
    box-shadow: 0 0 0 3px rgba(254, 202, 202, .72), 0 6px 18px rgba(127, 29, 29, .44);
}

/* Lifecycle and attention are separate: the marker stays red while an alarm is
   open, but only a newly received event animates. */
.camera-plan-pin.is-active::after,
.camera-plan-pin.is-pulsing::after {
    position: absolute;
    inset: -7px;
    border: 3px solid rgba(239, 68, 68, .52);
    border-radius: 999px;
    content: "";
    pointer-events: none;
}

.camera-plan-pin.is-pulsing::after {
    animation: detec-camera-alarm-pulse 1.05s ease-out infinite;
    border-color: rgba(239, 68, 68, .82);
}

.camera-plan-pin.is-selected {
    box-shadow: 0 0 0 4px rgba(56, 189, 248, .55), 0 7px 18px rgba(15, 23, 42, .4);
}

@keyframes detec-camera-alarm-pulse {
    0% {
        opacity: 1;
        transform: scale(.78);
    }

    70%,
    100% {
        opacity: 0;
        transform: scale(1.7);
    }
}

@media (prefers-reduced-motion: reduce) {
    .camera-plan-pin.is-pulsing::after { animation: none; }
}

/* The sector a camera covers on a drawing.

   Sized in the drawing's own percentages so it grows and shrinks with the plan
   as it is zoomed, and painted behind the mark so the mark stays legible over
   it. The fill is the same blue the camera itself uses on the map, and it turns
   the same red while an alarm is open, so the sector never disagrees with the
   pin sitting in it. */
.camera-plan-sector {
    position: absolute;
    overflow: visible;
    pointer-events: none;
    z-index: 0;
}

.camera-plan-sector path {
    fill: rgba(37, 99, 235, .17);
    stroke: rgba(37, 99, 235, .55);
    stroke-width: .8;
    vector-effect: non-scaling-stroke;
}

.camera-plan-sector.is-active path {
    fill: rgba(220, 38, 38, .2);
    stroke: rgba(220, 38, 38, .65);
}

/* Being tuned: brighter, so it reads against the sectors around it. */
.camera-plan-sector.is-selected path {
    fill: rgba(37, 99, 235, .26);
    stroke: rgba(37, 99, 235, .9);
    stroke-width: 1.4;
}

.camera-plan-sector.is-active.is-selected path {
    fill: rgba(220, 38, 38, .3);
    stroke: rgba(220, 38, 38, .95);
}

/* The mark sits above its own sector and above any neighbouring one. */
.camera-plan-pin { z-index: 1; }

/* Page heroes are deliberately stable. Earlier builds folded them with
   max-height and expanded them on hover/focus, which moved every workspace
   underneath the pointer and made headers appear to disappear. Keep the
   marker as the shared hero contract, but never change geometry in response
   to interaction. Individual pages may still use the compact workspace
   padding tokens above. */
[data-collapsible-hero] {
    overflow: visible;
    max-height: none;
    transition: none;
}

[data-collapsible-hero]:hover,
[data-collapsible-hero]:focus-within {
    max-height: none;
}
