/* Intentional product-level overrides on top of the UniFi-style UI Kit. */

/* Product typography: the kit's Inter stack (kit token, webfonts bundled). */
:root {
    --bs-body-font-family: var(--uf-font);
}
body {
    font-family: var(--uf-font);
    font-optical-sizing: auto;
    font-variation-settings: "GRAD" 0;
}

/* Authentication surface. */
.authentication-bg {
    background-color: var(--uf-auth-bg);
    background-image: linear-gradient(135deg, var(--uf-auth-bg) 0%, var(--uf-bg-subtle) 100%);
}
.authentication-bg .card {
    border: 0;
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.06);
}

/* Bootstrap has no max-width utility; keep every login method in one visual column. */
.login-methods {
    width: 100%;
    max-width: 400px;
}

#workos-google-login-form {
    width: 100%;
}

/*
 * Google Identity control rendered with the portal's outline button
 * (btn btn-outline theme-secondary) so it shares the theme's typography, sizing,
 * radius, and interaction states with every other action on the page. Only
 * the official multi-color "G" mark keeps Google's brand colors; its sizing
 * and label spacing live here.
 */
.google-g {
    display: inline-flex;
    width: 1.125rem;
    height: 1.125rem;
    margin-right: 0.625rem;
}

.google-g svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* Dense telemetry matrix severity states. Pair the bg-faded-warning /
   bg-faded-danger utilities on the cell with these accents so the label,
   numeric value, and unit read as one coherent state. */
.device-matrix__value {
    font-variant-numeric: tabular-nums;
    color: var(--uf-text-1);
}
.device-matrix__unit {
    font-weight: 600;
    color: var(--uf-text-2);
}
.device-matrix__accent--warning {
    color: var(--uf-orange);
}
.device-matrix__accent--danger {
    color: var(--uf-red);
}

/* Camera preview modal. The video keeps a stable 16:9 canvas while the
   placeholder explains the unconfigured template state. */
.camera-live-stage {
    position: relative;
    aspect-ratio: 16 / 9;
    min-height: 18rem;
    overflow: hidden;
    background:
        radial-gradient(circle at 50% 42%, rgba(255, 255, 255, 0.12), transparent 34%),
        #141518;
}
.camera-live-video {
    position: absolute;
    z-index: 2;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    background: transparent;
}
.camera-stream-placeholder {
    position: absolute;
    z-index: 1;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    text-align: center;
}
.camera-stream-placeholder.is-hidden {
    display: none;
}
.camera-placeholder-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4.5rem;
    height: 4.5rem;
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 50%;
    color: #fff;
    background: rgba(255, 255, 255, 0.08);
}
.camera-live-meta {
    position: absolute;
    z-index: 3;
    top: 1rem;
    left: 1rem;
    border-radius: 0.4rem;
    padding: 0.4rem 0.65rem;
    color: #fff;
    background: rgba(0, 0, 0, 0.72);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    pointer-events: none;
    backdrop-filter: blur(6px);
}
.camera-live-indicator {
    display: inline-block;
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: #ef4444;
    box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
    animation: camera-live-pulse 1.8s ease-out infinite;
}

@keyframes camera-live-pulse {
    70%, 100% { box-shadow: 0 0 0 0.35rem rgba(239, 68, 68, 0); }
}

@media (max-width: 575.98px) {
    .camera-live-stage {
        min-height: 14rem;
    }
}

/* Camera movement (PTZ) overlay on the live stage. The panel floats over the
   stream with the same dark-glass treatment as the LIVE badge so both read as
   one chrome layer. Direction buttons are press-and-hold, so they disable
   touch scrolling while pressed. */
.camera-ptz {
    position: absolute;
    z-index: 4;
    top: 1rem;
    right: 1rem;
    width: 11.75rem;
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 0.875rem;
    background: rgba(0, 0, 0, 0.72);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
    color: #fff;
    backdrop-filter: blur(6px);
}
.camera-ptz__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.5rem 0.6rem 0.4rem 0.85rem;
}
.camera-ptz__title {
    display: inline-flex;
    align-items: center;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.camera-ptz__title i {
    font-size: 0.9rem;
}
.camera-ptz__toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    border-radius: 0.4rem;
    background: rgba(255, 255, 255, 0.08);
    color: #fff;
    font-size: 1rem;
    line-height: 1;
}
.camera-ptz__toggle:hover {
    background: rgba(255, 255, 255, 0.18);
}
.camera-ptz__body {
    display: flex;
    gap: 0.45rem;
    padding: 0.15rem 0.85rem 0.85rem;
}
.camera-ptz.is-collapsed .camera-ptz__body {
    display: none;
}
.camera-ptz-dpad {
    display: grid;
    flex: 1 1 auto;
    grid-template-columns: repeat(3, 1fr);
    grid-auto-rows: 2.1rem;
    gap: 0.3rem;
}
.camera-ptz-zoom {
    display: flex;
    flex: 0 0 2.1rem;
    flex-direction: column;
    gap: 0.3rem;
}
.camera-ptz-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 2.1rem;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 0.55rem;
    background: rgba(255, 255, 255, 0.08);
    color: #fff;
    font-size: 1.15rem;
    line-height: 1;
    user-select: none;
    touch-action: none;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.camera-ptz-btn:hover {
    background: rgba(255, 255, 255, 0.18);
}
.camera-ptz-btn:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 1px;
}
/* Held state, applied by device-details.js while the command is active.
   Text follows --uf-on-primary so the gray fill keeps AA contrast in dark
   mode (light mode stays white on charcoal). */
.camera-ptz-btn.is-active {
    border-color: transparent;
    background: var(--uf-primary);
    color: var(--uf-on-primary);
}
.camera-ptz-btn--home {
    background: rgba(255, 255, 255, 0.14);
}
/* Diagonals reuse the cardinal chevrons, rotated to their compass heading. */
.camera-ptz-btn--nw > i { transform: rotate(-45deg); }
.camera-ptz-btn--ne > i { transform: rotate(45deg); }
.camera-ptz-btn--sw > i { transform: rotate(45deg); }
.camera-ptz-btn--se > i { transform: rotate(-45deg); }

@media (max-width: 575.98px) {
    .camera-ptz {
        top: 0.75rem;
        right: 0.75rem;
        width: 9.25rem;
    }
    .camera-ptz__body {
        padding: 0.1rem 0.7rem 0.7rem;
    }
    .camera-ptz-dpad {
        grid-auto-rows: 1.85rem;
    }
    .camera-ptz-zoom {
        flex-basis: 1.85rem;
    }
    .camera-ptz-btn {
        height: 1.85rem;
        font-size: 1rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .camera-ptz-btn {
        transition: none;
    }
}

/* Live device state: concentric signals keep the motion calm and purposeful. */
.device-live-card {
    background: linear-gradient(145deg, #fff 58%, rgba(var(--uf-green-rgb), 0.08));
}
.device-status-visual {
    position: relative;
    width: 8.5rem;
    height: 8.5rem;
    margin-right: auto;
    margin-left: auto;
}
.device-status-ring,
.device-status-core {
    position: absolute;
    top: 50%;
    left: 50%;
    border-radius: 50%;
    transform: translate(-50%, -50%);
}
.device-status-ring {
    border: 1px solid rgba(var(--uf-green-rgb), 0.38);
    animation: device-status-ripple 2.4s ease-out infinite;
}
.device-status-ring-inner {
    width: 5.4rem;
    height: 5.4rem;
}
.device-status-ring-outer {
    width: 8rem;
    height: 8rem;
    animation-delay: 0.8s;
}
.device-status-core {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4rem;
    height: 4rem;
    font-size: 1.8rem;
    animation: device-status-glow 2.4s ease-in-out infinite;
}
.device-live-dot {
    display: inline-block;
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 0 rgba(var(--uf-green-rgb), 0.4);
    animation: device-live-dot 1.8s ease-out infinite;
}

@keyframes device-status-ripple {
    0% { opacity: 0.8; transform: translate(-50%, -50%) scale(0.72); }
    75%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.08); }
}
@keyframes device-status-glow {
    0%, 100% { transform: translate(-50%, -50%) scale(1); }
    50% { transform: translate(-50%, -50%) scale(1.06); }
}
@keyframes device-live-dot {
    0% { box-shadow: 0 0 0 0 rgba(var(--uf-green-rgb), 0.4); }
    70%, 100% { box-shadow: 0 0 0 0.4rem rgba(var(--uf-green-rgb), 0); }
}

/* The axis-free sparkline bleeds to the card edges inside a clipped, text-free
   plot band. Its data can change freely without crossing readable content. */
.device-trend-card {
    background: linear-gradient(180deg, #fff 0%, #fff 42%, rgba(var(--uf-primary-rgb), 0.045) 100%);
}
.device-trend-reading {
    max-width: 18rem;
}
.device-trend-plot {
    min-height: 6.5rem;
    margin-right: -1.5rem;
    margin-left: -1.5rem;
    overflow: hidden;
}
.device-trend-chart {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0.78;
    pointer-events: none;
}
.device-trend-footer {
    position: relative;
    z-index: 2;
    background: rgba(255, 255, 255, 0.72);
}

/* Expanded device trend. The modal chart gets a stable host height so ECharts
   can calculate its canvas when Bootstrap finishes revealing the dialog. */
.device-detail-chart {
    width: 100%;
    height: 28rem;
    min-height: 28rem;
}

/* Leaflet is a decorative location layer on the compact card. The foreground
   gradient guarantees readable device details over every Positron tile. */
.device-map-background,
.device-map-scrim {
    position: absolute;
    inset: 0;
}
.device-map-background {
    z-index: 0;
    background: var(--uf-bg-subtle);
    pointer-events: none;
}
.device-map-background .leaflet-tile-pane {
    filter: grayscale(0.25) saturate(0.55);
    opacity: 0.72;
}
.device-map-scrim {
    z-index: 1;
    background: linear-gradient(
        180deg,
        rgba(255, 255, 255, 0.58) 0%,
        rgba(255, 255, 255, 0.12) 42%,
        rgba(255, 255, 255, 0.94) 72%,
        #fff 100%
    );
    pointer-events: none;
}
.device-map-card .leaflet-control-container {
    display: none;
}
.device-map-card .leaflet-marker-icon,
.device-map-card .leaflet-interactive {
    filter: none;
}
.device-map-attribution {
    color: var(--uf-text-2);
    font-size: 0.625rem;
    line-height: 1.2;
}
.device-map-attribution a {
    color: inherit;
}

/* Full-screen route explorer for the map card. Desktop keeps the map and
   summary visible together; smaller screens stack them into a natural scroll. */
.device-route-body,
.device-route-layout,
.device-route-map-wrap {
    min-height: 0;
}
.device-route-map-wrap {
    min-height: 30rem;
}
.device-route-map {
    position: absolute;
    inset: 0;
    background: var(--uf-bg-subtle);
}
.device-route-actions {
    position: absolute;
    z-index: 520;
    top: 1rem;
    right: 1rem;
}
.device-route-loading {
    position: absolute;
    z-index: 550;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    visibility: hidden;
    background: rgba(var(--uf-primary-rgb), 0.08);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease, visibility 0.2s ease;
    backdrop-filter: blur(2px);
}
.device-route-loading.is-visible {
    visibility: visible;
    opacity: 1;
}
.device-route-sidebar {
    overflow-y: auto;
    scrollbar-width: thin;
}
.device-route-map .leaflet-control-attribution {
    display: none;
}
.device-route-map .leaflet-popup-content {
    margin: 0.75rem 1rem;
    color: var(--uf-text-1);
    font-family: var(--uf-font);
    font-size: 0.75rem;
    line-height: 1.45;
}

@media (min-width: 992px) {
    .device-map-modal .dialog {
        height: 100vh;
    }
    .device-route-body {
        overflow: hidden;
    }
    .device-route-layout {
        overflow: hidden;
    }
}

@media (max-width: 991.98px) {
    .device-route-body {
        overflow-y: auto;
    }
    .device-route-map-wrap {
        min-height: 55vh;
    }
    .device-route-sidebar {
        overflow: visible;
    }
}

@media (prefers-reduced-motion: reduce) {
    .device-route-loading {
        transition: none;
    }
}

/* Compact trend hosts for the device details weather history row; mirrors the
   dashboard chart scaffolding but sized to sit three-up in a card grid. */
.device-mini-chart {
    width: 100%;
    height: 13rem;
    min-height: 13rem;
}

/* Purpose-built weather and GPS tracker details templates. */
.device-page-icon,
.metric-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    border-radius: 0.75rem;
}
.device-page-icon {
    width: 4rem;
    height: 4rem;
    font-size: 2rem;
}
.metric-icon {
    width: 2.5rem;
    height: 2.5rem;
    font-size: 1.25rem;
}
.metric-icon-sm {
    width: 2rem;
    height: 2rem;
    font-size: 1rem;
    border-radius: 50%;
}

/* Weather station: current conditions panel */
.weather-hero-panel {
    background:
        radial-gradient(circle at 8% 0, rgba(var(--uf-primary-rgb), 0.08), transparent 40%),
        linear-gradient(135deg, var(--uf-surface) 58%, var(--uf-bg-subtle));
}
.weather-condition-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4rem;
    height: 4rem;
    border-radius: 50%;
    color: var(--uf-primary);
    background: rgba(var(--uf-primary-rgb), 0.1);
    font-size: 2rem;
    flex: 0 0 auto;
}
.weather-temperature {
    color: var(--uf-text-1);
    font-size: clamp(3.75rem, 7vw, 5.5rem);
    font-weight: 600;
    line-height: 0.85;
    letter-spacing: -0.06em;
}

/* Weather station: secondary readings */
.weather-readings {
    border-left: 1px solid var(--uf-border);
    background: var(--uf-bg);
}
.weather-reading {
    padding: 1.5rem 1.75rem;
    border-bottom: 1px solid var(--uf-border);
}
.weather-reading:nth-child(-n+3) { border-bottom: 1px solid var(--uf-border); }
.weather-reading:nth-child(3n+1) { border-left: 0; }
.weather-reading:nth-child(3n) { border-right: 0; }
.weather-reading dt {
    color: var(--uf-text-2);
    font-size: 1rem;
    font-weight: 500;
    margin-bottom: 0.5rem;
}
.weather-reading dt i { margin-right: 0.35rem; }
.weather-reading dd {
    color: var(--uf-text-1);
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
    margin: 0;
    font-variant-numeric: tabular-nums;
}
.weather-reading-unit {
    display: block;
    color: var(--uf-text-2);
    font-size: 1rem;
    font-weight: 500;
    margin-top: 0.35rem;
}

/* Weather station: daily summary */
.weather-summary-item {
    padding: 0.875rem 1rem;
    border-radius: 0.5rem;
    background: var(--uf-bg-subtle);
    text-align: center;
}
.weather-summary-label {
    display: block;
    color: var(--uf-text-2);
    font-size: 0.75rem;
    margin-bottom: 0.25rem;
}
.weather-summary-value {
    display: block;
    color: var(--uf-text-1);
    font-size: 0.95rem;
    font-weight: 600;
}

/* Weather station: health list */
.weather-health-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.625rem 0;
    border-bottom: 1px solid var(--uf-border);
}
.weather-health-item:last-child { border-bottom: 0; }
.weather-health-item dt { font-size: 0.8rem; }
.weather-health-item dd { font-size: 0.85rem; }

/* Weather page warnings grid: idle (not in force) tiles read grayscale +
   muted next to the lit active tiles. The advisory text stays legible —
   only the tile chrome is dimmed, never the meaning alone. */
.wx-warn-idle {
    filter: grayscale(1);
    opacity: 0.6;
}

/* Official HKO warning symbols (static/img/weather/): fixed box keeps the
   4-per-row tiles aligned however the source GIF is shaped. */
.wx-warn-icon {
    width: 40px;
    height: 40px;
    object-fit: contain;
}

/* Sensor details: telemetry metric cards */
.sensor-metric-card {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.sensor-metric-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.08) !important;
}
.sensor-metric-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 0.75rem;
    font-size: 1.35rem;
    flex: 0 0 auto;
}
.sensor-metric-progress {
    height: 6px;
    border-radius: 3px;
    background-color: var(--uf-bg-muted);
}
.sensor-metric-progress .progress-bar {
    border-radius: 3px;
}

.gps-route-map-wrap { min-height: 34rem; }
.gps-trip-summary dt { color: var(--uf-text-2); font-size: 0.7rem; font-weight: 400; margin-bottom: 0.3rem; }
.gps-trip-summary dd { color: var(--uf-text-1); font-size: 0.9rem; font-weight: 700; margin: 0; }
.gps-trip-summary > div + div { border-left: 1px solid var(--uf-border); }
.gps-speed-panel {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    padding: 1.1rem 1.25rem;
    color: var(--uf-primary);
    background: rgba(var(--uf-primary-rgb), 0.08);
}
.gps-speed-panel > span { color: var(--uf-text-2); font-size: 0.75rem; }
.gps-speed-panel strong { grid-row: span 2; font-size: 2.25rem; line-height: 1; }
.gps-speed-panel strong small { font-size: 0.75rem; font-weight: 600; }
.gps-speed-panel em { color: var(--uf-text-1); font-size: 0.8rem; font-style: normal; font-weight: 600; }

@media (max-width: 991.98px) {
    .weather-readings {
        border-left: 0;
        border-top: 1px solid var(--uf-border);
    }
}

@media (max-width: 767.98px) {
    .weather-reading { padding: 1rem 1.15rem; }
    .weather-reading dd { font-size: 1.25rem; }
    .gps-trip-summary > div:nth-child(3) { border-left: 0; }
    .gps-trip-summary > div:nth-child(-n + 2) { padding-bottom: 1rem; }
    .gps-trip-summary > div:nth-last-child(-n + 2) { padding-top: 1rem; border-top: 1px solid var(--uf-border); }
}

@media (max-width: 575.98px) {
    .device-mini-chart {
        height: 10rem;
        min-height: 10rem;
    }

    /* The 55vh inline-route fallback is generous on phones; tighten it. */
    .device-route-map-wrap {
        min-height: 20rem;
    }
}

/* Compact device conversation. Messages remain inside a bounded log so user
   input cannot cause the card to grow and break the surrounding grid. */
.device-chat-card {
    background: linear-gradient(145deg, #fff 62%, rgba(var(--uf-primary-rgb), 0.055));
}
.device-chat-log {
    min-height: 8.75rem;
    max-height: 8.75rem;
    overflow-y: auto;
    background: var(--uf-bg-subtle);
    scrollbar-width: thin;
}
.device-chat-log.is-scrolled {
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 1.35rem, #000 100%);
    mask-image: linear-gradient(to bottom, transparent 0, #000 1.35rem, #000 100%);
}
.device-chat-message {
    width: fit-content;
    max-width: 88%;
    border-radius: 0.75rem;
    padding: 0.55rem 0.7rem;
    font-size: 0.75rem;
    line-height: 1.4;
}
.device-chat-message + .device-chat-message {
    margin-top: 0.55rem;
}
.device-chat-message-output {
    border: 1px solid var(--uf-border);
    background: #fff;
    color: var(--uf-text-1);
}
.device-chat-message-input {
    margin-left: auto;
    background: var(--uf-primary);
    color: var(--uf-on-primary);
}
/* Dark mode user bubble: same light-fill + dark-text inversion as the solid
   CTA so own messages stay AAA-readable on the dark page. */
[data-bs-theme="dark"] .device-chat-message-input {
    background: #e8e8e8;
}
.device-chat-speaker {
    display: block;
    margin-bottom: 0.15rem;
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    opacity: 0.72;
    text-transform: uppercase;
}

/* Predictive health is deliberately glanceable: the conic ring communicates
   the score while the adjacent definition list carries exact diagnostics. */
.device-health-gauge {
    position: relative;
    width: 6.5rem;
    height: 6.5rem;
    border-radius: 50%;
    background: conic-gradient(var(--uf-orange) 0 82%, var(--uf-bg-muted) 82% 100%);
}
.device-health-gauge::before {
    position: absolute;
    inset: 0.65rem;
    border-radius: 50%;
    background: #fff;
    box-shadow: inset 0 0 0 1px var(--uf-border);
    content: "";
}
.device-health-gauge-value {
    position: absolute;
    z-index: 1;
    top: 50%;
    left: 50%;
    display: flex;
    align-items: baseline;
    transform: translate(-50%, -50%);
}
.device-health-gauge-value strong {
    color: var(--uf-text-1);
    font-size: 1.5rem;
    line-height: 1;
}
.device-health-gauge-value span {
    color: var(--uf-text-2);
    font-size: 0.625rem;
}

@media (prefers-reduced-motion: reduce) {
    .device-status-ring,
    .device-status-core,
    .device-live-dot {
        animation: none;
    }
    .device-status-ring-inner { opacity: 0.55; }
    .device-status-ring-outer { opacity: 0.28; }
}

/* Dashboard analytics. ECharts needs an explicit height on each host element. */
.dashboard-chart {
    width: 100%;
    height: 19rem;
    min-height: 19rem;
}
.dashboard-chart-lg {
    height: 22rem;
    min-height: 22rem;
}
.chart-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    flex: 0 0 2.5rem;
    border-radius: 0.75rem;
    font-size: 1.25rem;
}

@media (max-width: 575.98px) {
    .device-detail-chart {
        height: 21rem;
        min-height: 21rem;
    }
    .dashboard-chart,
    .dashboard-chart-lg {
        height: 17rem;
        min-height: 17rem;
    }
}

/* Device dashboard discovery toolbar. Search remains wider than sort while
   the sort and display controls stay anchored to the right on larger screens. */
.device-toolbar {
    display: flex;
    align-items: end;
    gap: 1rem;
    width: 100%;
}

.device-toolbar__search {
    flex: 1 1 36rem;
    max-width: 36rem;
}

.device-toolbar__controls {
    display: flex;
    align-items: end;
    gap: 1rem;
    flex: 0 0 auto;
    margin-left: auto;
}

.device-toolbar__sort {
    flex: 0 0 18rem;
    max-width: 18rem;
}

.device-toolbar__status {
    flex: 0 0 9rem;
    max-width: 9rem;
}

.device-toolbar__view {
    flex: 0 0 auto;
    min-width: 0;
}

.device-view-switch {
    display: inline-flex;
}

@media (max-width: 767.98px) {
    .device-toolbar {
        flex-direction: column;
        align-items: stretch;
    }

    .device-toolbar__search {
        flex-basis: auto;
        max-width: none;
    }

    .device-toolbar__controls {
        justify-content: space-between;
        width: 100%;
    }

    .device-toolbar__sort {
        flex: 1 1 auto;
        max-width: none;
    }

    .device-toolbar__status {
        flex: 1 1 auto;
        max-width: none;
    }
}

/* Card table filter bar: search stays left, filters stick right on wide
   rows; the search wraps to its own full-width row on narrow cards and the
   filters continue on the following row. Same anatomy as .device-toolbar
   (search left, controls right) but sized for in-card toolbars. */
.table-filterbar {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: 1rem;
    width: 100%;
}

.table-filterbar__search {
    flex: 1 1 20rem;
    max-width: 24rem;
    min-width: 0;
}

.table-filterbar__search .form-control {
    min-width: 0;
}

.table-filterbar__filters {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: 1rem;
    margin-left: auto;
}

.table-filterbar__filter {
    flex: 0 0 auto;
    min-width: 0;
}

/* Right-sized controls: selects shrink to their content instead of filling
   the row; text inputs keep the wrapper's defined width. */
.table-filterbar__filter > .form-control {
    width: auto;
    max-width: 100%;
}

/* =========================================================================
   Combined map + telemetry card (production VMM dashboard reference).
   Reuses the `.device-map-*` map layer and scrim classes above so a
   decorative, non-interactive CARTO background stays the visual baseline;
   the classes below add the instrument foreground only.
   Source: VMMDashboard app/static/css/app.css and js/dashboard.js buildCard().
   ========================================================================= */

/* Combined card. The map host fills the card and the body floats over the
   white scrim so telemetry keeps strong foreground contrast over every tile. */
.vmm-card {
    min-height: 18rem;
}
.vmm-card__map {
    position: absolute;
    inset: 0;
    z-index: 0;
}
.vmm-card__scrim {
    /* Inherits the device-map-scrim gradient; keep it above the map tiles. */
    z-index: 1;
}
.vmm-card__body {
    position: relative;
    z-index: 2;
    /* Raise the body above the map so the lower metrics band always clears the
       marker, which is shifted into the upper visual third by dashboard.js. */
    justify-content: flex-end;
}
.vmm-card__name {
    font-weight: 600;
    line-height: 1.25;
}
.vmm-card__received {
    white-space: nowrap;
}

@media (max-width: 575.98px) {
    /* On phones, prioritize device information over the decorative map field. */
    .vmm-card {
        min-height: 0;
    }
    .vmm-card__body {
        justify-content: flex-start;
    }
}

/* Prominent electrical readings. Each value carries its own accent color so
   voltage/power/battery stay distinguishable in the muted map field. */
.vmm-card__metrics {
    margin-top: 0.75rem;
}
.vmm-metric {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}
.vmm-metric__value {
    font-size: 1.35rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
    letter-spacing: -0.01em;
}
.vmm-metric__label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--uf-text-2);
}

/* Neutral fallback when a device has no GPS coordinates: a calm electrical-grid
   texture instead of a broken/empty map pane. No glyph is injected here (a
   hardcoded icon codepoint would be unverifiable); the layered grid + soft
   instrument glows read as a deliberate, non-broken fallback surface. */
.vmm-card__map--neutral {
    background-color: var(--uf-bg-subtle);
    background-image:
        linear-gradient(0deg, rgba(var(--uf-primary-rgb), 0.06) 1px, transparent 1px),
        linear-gradient(90deg, rgba(var(--uf-primary-rgb), 0.06) 1px, transparent 1px),
        radial-gradient(circle at 30% 25%, rgba(var(--uf-primary-rgb), 0.10), transparent 55%),
        radial-gradient(circle at 75% 80%, rgba(34, 197, 94, 0.08), transparent 60%);
    background-size: 26px 26px, 26px 26px, auto, auto;
}

/* Compact VMM list-view row. Identity is allowed to shrink so long names are
   truncated while the latest readings and action buttons remain aligned. */
.vmm-list-card__name {
    font-weight: 600;
    line-height: 1.25;
}

.vmm-list-card__status-dot {
    display: inline-block;
    flex-shrink: 0;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
}

.vmm-list-card__status-dot--online {
    background-color: var(--uf-green);
}

.vmm-list-card__status-dot--offline {
    background-color: var(--uf-red);
}

.vmm-list-card__value {
    font-size: 1.1rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
    white-space: nowrap;
}

.vmm-list-card__reading {
    flex-shrink: 0;
}

.vmm-list-card__identity {
    min-width: 0;
}

/* Five compact-row concepts. The kit's stock list-group, card-body, badge,
   button, border, background, spacing, and typography utilities provide the
   shared visual language; only the layouts without kit equivalents live here. */
.device-list > .list-group-item {
    min-height: 5.5rem;
    /* List rows are standalone cards (they carry shadow-sm + rounded-3), so
       they need the card surface. `.list-group-item` defaults to
       `--bs-list-group-bg` (= `--bs-bg-body` = `--uf-bg`), which makes the
       rows read as one flat block against the page and drops the elevation
       the shadow implies. Use the same surface as `.card` so the group cards
       match the processing-overview and worker-status cards. `--bs-card-bg`
       is declared on `.card` itself, so the underlying token is used here. */
    background-color: var(--uf-surface);
}

/* Spaced-card lists (dashboard/groups `list-group gap-3`): Bootstrap zeroes
   the top border of every item after the first to merge flush list rows
   (`.list-group-item + .list-group-item { border-top-width: 0 }`). With
   `gap-3` the items are standalone cards, so each one needs all four
   borders — without this the 2nd+ card (e.g. dashboard "Test") loses its
   top edge. */
.device-list > .list-group-item + .list-group-item {
    border-top-width: var(--bs-list-group-border-width);
}

/* A warning rail is not available as a one-sided border utility when
   the list-group item already owns a neutral border on all four sides. */
.device-list-alert {
    border-left-color: var(--uf-orange) !important;
    border-left-width: 0.25rem !important;
}

.list-inline-reading {
    align-items: center;
    gap: 0.65rem;
    min-width: 8.5rem;
}

.list-inline-reading > i {
    font-size: 1.5rem;
}

.list-inline-reading strong,
.list-inline-reading span,
.list-primary-reading strong,
.list-primary-reading span,
.list-alert-reading strong,
.list-alert-reading span {
    display: block;
    white-space: nowrap;
}

.list-inline-reading strong {
    font-size: 0.9rem;
}

.list-inline-reading span,
.list-primary-reading span,
.list-alert-reading span {
    color: var(--uf-text-2);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
}

.list-metric-layout {
    display: grid;
    grid-template-columns: minmax(13rem, 1fr) auto auto;
    align-items: stretch;
}

.list-metric-identity {
    align-self: center;
    min-width: 0;
}

/* Group rows (dashboard + groups list): size to content instead of the
   generic 5.5rem device-list min-height, and inset the identity tile to the
   same 18px gutter the stat cards use so the icons line up. */
.group-list > .list-group-item {
    min-height: 0;
}

.group-list .list-metric-identity {
    padding-left: 1.125rem;
}

.list-metric-strip {
    display: grid;
    grid-template-columns: repeat(3, minmax(6rem, 1fr));
}

.list-metric-strip > div {
    display: flex;
    flex-direction: column-reverse;
    justify-content: center;
    min-width: 6rem;
    padding: 0.85rem 1rem;
    text-align: center;
}

.list-metric-strip dt {
    color: var(--uf-text-2);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.list-metric-strip dd {
    margin: 0 0 0.15rem;
    font-size: 1rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.list-metric-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.list-alert-reading strong,
.list-primary-reading strong {
    font-size: 1.1rem;
    font-variant-numeric: tabular-nums;
}

.list-primary-reading {
    min-width: 7.5rem;
}

@media (max-width: 767.98px) {
    .list-metric-layout {
        grid-template-columns: 1fr auto;
    }

    .list-metric-strip {
        grid-column: 1 / -1;
        grid-row: 2;
        border-left: 0 !important;
        border-top: 1px solid var(--uf-border-light);
    }

    .list-metric-actions {
        grid-column: 2;
        grid-row: 1;
    }
}

@media (max-width: 575.98px) {
    .device-list .card-body {
        gap: 0.75rem !important;
    }

    .list-metric-strip {
        grid-template-columns: repeat(3, 1fr);
    }

    .list-metric-strip > div {
        min-width: 0;
        padding: 0.7rem 0.4rem;
    }

    .list-metric-strip dd {
        font-size: 0.875rem;
    }

}

/* List/grid view toggle (groups, dashboard groups, workers).
 *
 * List stays the default: the existing Design 3 metric-strip rows (groups)
 * and the workers table. Grid reuses the same kit pieces (card, soft
 * identity tile, metric strip, dots/badges, btn-default) in a responsive
 * Bootstrap row; the rules below only adapt the strip to a stacked card
 * and keep long names truncated inside grid columns. */
.group-grid-card .list-metric-strip {
    border-top: 1px solid var(--uf-border-light);
}

.group-grid-card .list-metric-strip > div {
    min-width: 0;
    padding: 0.7rem 0.5rem;
}

.group-grid-card .vmm-list-card__identity,
.worker-grid-card .worker-grid-identity {
    min-width: 0;
}

@media (max-width: 575.98px) {
    .group-grid-card .list-metric-strip dd {
        font-size: 0.875rem;
    }
}

/* Skeleton placeholders for a first load, if a live implementation reuses this
   card. The shimmer is disabled under prefers-reduced-motion (see below). */
.vmm-card--skeleton .card-body {
    pointer-events: none;
}
.vmm-skeleton-bar {
    position: relative;
    overflow: hidden;
    background: linear-gradient(100deg, var(--uf-bg-muted) 30%, var(--uf-bg-subtle) 50%, var(--uf-bg-muted) 70%);
    background-size: 200% 100%;
    animation: vmm-skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes vmm-skeleton-shimmer {
    0% { background-position: 180% 0; }
    100% { background-position: -180% 0; }
}

@media (max-width: 575.98px) {
    .vmm-metric__value {
        font-size: 1.15rem;
    }
}

/* Respect reduced-motion across the combined card surface: stop the skeleton
   shimmer and any Leaflet decorative animation. Keep content fully legible. */
@media (prefers-reduced-motion: reduce) {
    .vmm-skeleton-bar {
        animation: none;
        background: var(--uf-bg-muted);
    }
    .vmm-card,
    .vmm-card__map {
        transition: none;
    }
}

/*
 * Restore the `hidden` attribute against Bootstrap display utilities
 * (portal bug found in the first real browser pass, 10 September 2026).
 *
 * Some themes define `[hidden] { display: none !important }` in their reboot
 * section, and every display utility (`.d-flex` … ) later in the stylesheet
 * with the same !important tier wins by source order — so any element
 * combining `hidden` with a `d-*` utility would stay visible. This file loads
 * after the theme, so this rule restores the attribute everywhere. Known affected element: the job page's late-result banner
 * (#job-result-late), which was permanently visible.
 */
[hidden] {
    display: none !important;
}

/*
 * Worker allocation picker (group detail "Manage workers" modal).
 * Locked rows — the worker belongs to another process group or is
 * reserved by an active attempt — mute only the NAME; the reason tag
 * ("Assigned to <group>" / "Busy") renders at full contrast so the
 * blocker reads at a glance. The narrow leading column hosts the tick
 * boxes.
 */
.worker-picker-col-select {
    width: 2.75rem;
}

.worker-picker-col-select .check {
    margin: 0 auto;
    display: block;
}

.worker-picker-row-locked .worker-picker-name {
    color: var(--bs-fg-2, #707070);
}

.worker-picker-tag {
    font-weight: 500;
    white-space: nowrap;
}

#group-workers-error {
    white-space: pre-wrap;
}

/*
 * Group detail filter bars (queue + Results cards). The markup uses the
 * Bootstrap grid (`row g-3`: search `col-12`, Status/Type `col-6` + `col-6`,
 * date/time `col-12` on phones), so the mobile layout comes from the grid
 * alone. These fixed widths apply from md up so both cards keep defined,
 * identical, coherent sizes on tablets/desktops instead of stretching with
 * the grid or depending on the native time-input width.
 */
@media (min-width: 768px) {
    .vpp-f-search {
        width: 18rem;
        max-width: 100%;
    }

    .vpp-f-select {
        width: 11rem;
    }

    .vpp-f-datetime {
        width: 17.5rem;
        max-width: 100%;
    }

    /* Date range + start/end time group (date col-6, times col-3 each):
     * one block that sits on the same row as Search/Status/Type. */
    .vpp-f-daterange {
        width: 36rem;
        max-width: 100%;
    }
}

/* Inside the date/time group the time inputs fill their grid column
 * instead of the fixed 7rem used by the older stacked layout. */
.vpp-f-daterange .vpp-filter-time-input {
    width: 100%;
}

.vpp-filter-datetime-row {
    display: flex;
    gap: 0.5rem;
}

/* Range input + its two time inputs stack: the range spans full width,
 * the times share one row below (each flexible, same 7rem basis as the
 * former single-date rows). */
.vpp-filter-range-input {
    width: 100%;
    min-width: 0;
}

.vpp-filter-timerow {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

.vpp-filter-timerow .vpp-filter-time-input {
    flex: 1 1 0;
}

/* Labeled start/end time fields below a range input: each column stacks
 * its caption above its time input so both stay visible (not
 * aria-label-only). */
.vpp-filter-timefield {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.vpp-filter-timelabel {
    margin-bottom: 0.25rem;
}

.vpp-filter-date-input {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
}

.vpp-filter-time-input {
    flex: 0 1 auto;
    width: 7rem;
    min-width: 0;
}

/*
 * Single-row date range + start/end time (used by every filter bar:
 * job Lifecycle, worker history/command/telemetry, group processed,
 * audit, API logs). Date flexes, times keep compact fixed widths so
 * all three stay on ONE line at every breakpoint, phones included.
 * Each field stacks its label above its input.
 */
.vpp-dt-row {
    display: flex;
    gap: 0.5rem;
    flex-wrap: nowrap;
    align-items: flex-start;
}

.vpp-dt-date {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.vpp-dt-time {
    flex: 0 0 6.5rem;
    width: 6.5rem;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.vpp-dt-row input {
    width: 100%;
    min-width: 0;
}

.vpp-dt-date input {
    text-overflow: ellipsis;
}

@media (max-width: 767.98px) {
    .vpp-dt-time {
        flex-basis: 5.5rem;
        width: 5.5rem;
    }
}

/*
 * Group detail header (process-group top buttons): the shell uses the kit
 * `.uf-page-head`, whose xs breakpoint already stacks `.ms-auto` full-width
 * below the title. The actions container wraps so "Manage access / Manage
 * workers / Edit" never overflow horizontally; on phones the buttons share
 * the row equally (two per row, wrapping) instead of being cut off.
 */
#group-detail-page #group-detail-head {
    flex-wrap: wrap;
    align-items: flex-start;
    /* Subtitle row (Group ID) sits directly under the title:
     * keep only a 4px rhythm gap instead of the kit's 16px. */
    margin-bottom: 4px;
}

#group-detail-page #group-detail-head > div:first-child {
    min-width: 0;
    flex: 1 1 0;
}

#group-detail-page #group-detail-head .group-actions {
    flex-shrink: 0;
}

#group-detail-page #group-detail-head .group-actions .btn {
    white-space: nowrap;
}

@media (max-width: 575.98px) {
    #group-detail-page #group-detail-head .group-actions {
        width: 100%;
        margin-left: 0 !important;
    }
    #group-detail-page #group-detail-head .group-actions .btn {
        flex: 1 1 140px;
        justify-content: center;
    }
}

/* Group ID row: the UUID must break anywhere so it never
 * overflows on narrow phones. */
#group-detail-page #group-id-row {
    min-width: 0;
}

#group-detail-page #group-id-row code {
    min-width: 0;
    overflow-wrap: anywhere;
}

/*
 * Group detail Error column (queue + processed tables): inline code plus a
 * clamped message so the Failed badge always ships with a visible reason.
 * The column keeps a bounded width so long backend messages wrap/clamp
 * instead of stretching the table; the full text stays in the cell's
 * title/aria-label and on the job-detail page.
 */
.vpp-error-cell {
    max-width: 22rem;
    min-width: 10rem;
}

.vpp-error-cell .min-w-0 {
    min-width: 0;
}

.vpp-error-msg {
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/*
 * File-name cells (group queue + processed tables, worker job history,
 * storage file table, job artifacts table): bound the width so
 * long file names (e.g. 100+ char camera exports) truncate with an
 * ellipsis instead of stretching the table and breaking the layout.
 * A common name like AKNV_S07_20260909_090000-090100.mp4 (~35 chars)
 * still fits without truncation on desktop; longer names clamp and the
 * full text stays in the element's title tooltip. The client-reference
 * subline truncates the same way.
 */
.vpp-file-cell {
    max-width: 26rem;
    min-width: 10rem;
}

.vpp-file-link,
.vpp-file-ref {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

.vpp-file-ref {
    display: block;
}

@media (max-width: 575.98px) {
    .vpp-file-cell {
        max-width: 12rem;
        min-width: 0;
    }
}

/*
 * Audit log table: fixed layout with defined column widths so narrow
 * columns (timestamp, user, action, resource type) always fit their
 * content on one line while the smaller resource-id truncates and the
 * content (JSON) column takes the remainder. Timestamp/user/ids/action
 * stay on one line with ellipsis; content wraps anywhere instead of
 * stretching.
 */
.audit-table {
    table-layout: fixed;
    width: 100%;
    min-width: 80rem;
}

.audit-col-time { width: 15rem; }
.audit-col-user { width: 14rem; }
.audit-col-action { width: 16rem; }
.audit-col-restype { width: 11rem; }
.audit-col-resid { width: 7rem; }
.audit-col-content { width: auto; }

.audit-table th,
.audit-table td {
    vertical-align: middle;
}

.audit-cell-nowrap {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.audit-cell-nowrap .uf-badge {
    display: inline-block;
    vertical-align: middle;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.audit-cell-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.audit-cell-time {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.audit-cell-time .audit-time-absolute {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.audit-cell-content {
    min-width: 0;
}

.audit-cell-content code {
    display: block;
    overflow-wrap: anywhere;
    word-break: break-word;
    white-space: pre-wrap;
    font-size: 0.75rem;
}

/* Clickable audit rows open the event-details modal. */
.audit-row {
    cursor: pointer;
}

.audit-row:hover {
    background-color: var(--bs-bg-1);
}

.audit-row:focus-visible {
    outline: 2px solid var(--bs-primary-bg);
    outline-offset: -2px;
}

/*
 * Command log table (device-detail "Commands & configuration" card): the
 * audit table's card anatomy (full-bleed table, bordered header strip,
 * padded pager footer). Columns size to content.
 */
.wi-commands-table th,
.wi-commands-table td {
    vertical-align: middle;
}

/*
 * Command-log column widths (audit-table pattern: fixed layout so the
 * result message wraps instead of stretching, actions gutter right).
 */
.wi-commands-table {
    table-layout: fixed;
    width: 100%;
    min-width: 62rem;
}

.wi-col-id { width: 7rem; }
.wi-col-command { width: 14rem; }
.wi-col-status { width: 8rem; }
.wi-col-queued { width: 15rem; }
.wi-col-result { width: auto; }
.wi-col-actions { width: 6rem; }

/*
 * Command-log id column (audit-table pattern): the "#" + up to 8-digit
 * command number stays on one line with tabular digits instead of
 * wrapping inside the fixed 7rem column.
 */
.wi-commands-table th:first-child,
.wi-commands-table td:first-child {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/*
 * Receiver-arrival log table (command-logs page): the audit-table pattern
 * (fixed layout with defined column widths) so time, serial and status
 * always fit on one line while the content column takes the remainder.
 * Rows reuse the audit clickable-row affordance (audit-row) to open the
 * arrival details dialog.
 */
.cmdlog-table {
    table-layout: fixed;
    width: 100%;
    min-width: 48rem;
}

.cmdlog-col-time { width: 15rem; }
.cmdlog-col-serial { width: 12rem; }
.cmdlog-col-status { width: 8rem; }
.cmdlog-col-content { width: auto; }

.cmdlog-table th,
.cmdlog-table td {
    vertical-align: middle;
}

.cmdlog-cell-nowrap {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cmdlog-cell-content {
    min-width: 0;
}

/*
 * Command-log scroll wrapper: keep it the containing block for absolutely
 * positioned descendants. The actions header carries a visually-hidden
 * (absolute) label; without a positioned wrapper its containing block is
 * the relative .card outside the scroller, so its scrolled-out position
 * bypasses the wrapper's overflow containment and widens the mobile layout
 * viewport (the whole page squeezes into ~half the screen width).
 */
#ingest-card .table-responsive {
    position: relative;
}

.audit-detail-content {
    max-height: 24rem;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 0.8rem;
}

/*
 * Stream detection table: fixed layout with defined column widths so the
 * Artifacts column stays a minimal icon-only gutter stuck to the right —
 * Time keeps one line for the wall-clock timestamp, Names takes the
 * remainder. The file name lives in the button title/aria-label and in
 * the preview modal title.
 */
.stream-events-table {
    table-layout: fixed;
    width: 100%;
}

.stream-col-time { width: 12rem; }
.stream-col-names { width: auto; }
.stream-col-artifacts { width: 3.25rem; }

.stream-events-table th:last-child,
.stream-events-table td:last-child {
    text-align: right;
}

.stream-artifact-btn {
    padding-left: 0.4rem;
    padding-right: 0.4rem;
    vertical-align: middle;
}

.stream-artifact-btn + .stream-artifact-btn {
    margin-left: 0.25rem;
}

/* Markdown docs (api-docs, changelog) on the Bootstrap 6 .prose class: see
 * the kit's DOCS CONTENT BOXES section in vendor/css/unifi-ui.css (the
 * single source — do not redeclare .prose rules here). Tables keep their
 * classless markup (prose styles table:not([class])); the page scripts
 * wrap them in .table-responsive for narrow screens. */

/* Compact soft toasts (see static/js/toast.js). The header tint comes from
   the kit's .toast-success / .toast-danger / … soft rules in unifi-ui.css;
   here the toast is only made smaller and calmer: narrower than Bootstrap's
   350px default, smaller type, tighter header padding and a soft shadow so
   it sits quietly in the bottom-right corner in both themes. */
.toast-compact {
    width: auto;
    max-width: 20rem;
    font-size: 0.8125rem;
    border-radius: 0.625rem;
    box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.12);
}
.toast-compact .toast-header {
    padding: 0.45rem 0.7rem;
    font-size: 0.75rem;
    font-weight: 600;
}
.toast-compact .toast-header .fs-lg {
    font-size: 1rem;
}
.toast-compact .toast-body {
    padding: 0.55rem 0.7rem;
}

/* Toast container in the top layer (see static/js/toast.js): the container
   is a manual popover so toasts paint above native <dialog> overlays
   (Dialog/Drawer via showModal) and their ::backdrops, which ignore z-index.
   The UA popover stylesheet centers with inset:0/margin:auto and adds a
   bordered box — reset it so the Bootstrap position utilities (bottom-0 /
   top-0 + end-0) keep pinning the corner, and never show a backdrop.
   WORKAROUND — retire with the toast.js popover promotion once the kit
   top-layers toasts itself; see the comment there. */
#toast-container[popover] {
    margin: 0;
    border: 0;
    background: transparent;
    overflow: visible;
}
/* UA popover styles center with inset:0/margin:auto. Pin the corner
   explicitly for each placement: unlayered author rules beat the layered
   position utilities (.bottom-0/.top-0/.end-0), so every side is set here
   instead of relying on those utilities in the popover case. (Without
   Popover API support toast.js drops the attribute and the utilities apply
   normally.) */
#toast-container[popover].bottom-0 {
    top: auto;
    bottom: 0;
    left: auto;
    right: 0;
}
#toast-container[popover].top-0 {
    top: 0;
    bottom: auto;
    left: auto;
    right: 0;
}
#toast-container::backdrop {
    display: none;
    background: transparent;
}

/*
 * Job detail page head on narrow screens: the title can be a long
 * unbroken stream URL or file path, and streams carry up to seven
 * action buttons — without wrapping, both push the page sideways.
 * The title block may shrink (min-width: 0) and breaks anywhere;
 * the action row wraps between buttons, each keeping icon + label
 * on one line. No-op on wide screens (everything fits one line).
 */
#job-detail-page .uf-page-head > div:first-child {
    min-width: 0;
    /* Zero basis (not content-sized): the file name must never squeeze the
     * action buttons off the row. The title takes whatever is left after
     * the buttons' natural width and clamps to three lines; the buttons
     * stay top-right (tablet/desktop). Phones stack via the query below. */
    flex: 1 1 0;
}
#job-detail-page #job-detail-heading {
    min-width: 0;
    overflow-wrap: anywhere;
    /* Long file names clamp to three lines with an ellipsis instead of
     * pushing the action buttons or stretching the page head; the full
     * name stays in the heading's title tooltip and in the Details row.
     * Doubled ID wins over the kit's `.uf-page-title` display rule, and
     * the legacy box model is required: `line-clamp` alone does not
     * engage in this browser (verified: block/flow-root render all
     * lines, -webkit-box clamps to 3). */
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    line-clamp: 3;
}
/* Details "File" value: keep the full name but break long unbroken
 * strings anywhere so the facts column never overflows. The !important
 * wins over Bootstrap's `.text-break` (which sets break-word with
 * !important and would otherwise keep mid-extension splits). */
#job-meta-file {
    overflow-wrap: anywhere !important;
}
/* Delete/terminate confirmations quote the file name in the modal body:
 * break long unbroken names anywhere so the modal never overflows. */
#job-confirm-message {
    overflow-wrap: anywhere;
}
#job-detail-page .uf-page-head {
    flex-wrap: wrap;
    /* Buttons pin to the top row (first title line), never vertically
     * centered against a multi-line file name. */
    align-items: flex-start;
}
/* Tablet/desktop: never wrap the buttons below the title — the title
 * above shrinks (zero-basis flex) while the actions keep their natural
 * size at the top right. */
@media (min-width: 768px) {
    #job-detail-page .uf-page-head {
        flex-wrap: nowrap;
    }
}
#job-detail-page .uf-page-head .ms-auto {
    flex-wrap: wrap;
    flex-shrink: 0;
}
@media (max-width: 767.98px) {
    #job-detail-page .uf-page-head > div:first-child {
        max-width: 100%;
        flex-basis: 100%;
    }
}
#job-detail-page .uf-page-head .ms-auto .btn {
    white-space: nowrap;
}

/*
 * Tables on narrow phones: cell content (file names, URLs, IDs) must not
 * wrap — wrapped fragments like "det / ect / ion / s.j / so / n" are
 * unreadable. Every data table already lives in a horizontally scrollable
 * `.table-responsive` wrapper, so keep cells on one line and let wide tables
 * scroll instead. No-op on wider screens.
 */
@media (max-width: 575.98px) {
    .table-responsive > .table th,
    .table-responsive > .table td {
        white-space: nowrap;
    }
}

/*
 * Dialogs whose header/body/footer are wrapped in a <form> (worker, group
 * and other create/edit modals need the footer submit inside the form):
 * the Bootstrap dialog is a flex column that expects .dialog-header /
 * .dialog-body / .dialog-footer as DIRECT children (see the
 * dialog-scrollable reference markup). The intermediate <form> is the
 * actual flex item, so without this the body never gets a height
 * constraint — .dialog-scrollable's overflow-y:auto has nothing to
 * scroll, tall content spills past the rounded border and the footer is
 * pushed out of view. Make the form a pass-through flex column instead:
 * header/footer keep their shrink-0 behavior, the body shrinks (its
 * non-visible overflow already zeroes the automatic minimum) and scrolls.
 */
.dialog > form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    max-height: 100%;
}

/*
 * Modal footers on narrow phones: footers with a leading destructive action
 * (Delete worker / Delete group + Cancel + Save) exceed the ~310px content
 * width at 360px, so the primary button wraps onto a second row and lands
 * centered, detached from Cancel. Stack the footer vertically on xs screens
 * so every action is full-width in DOM order (destructive first, primary
 * last) with a comfortable touch target. No-op on wider screens.
 */
@media (max-width: 575.98px) {
    .dialog-footer {
        flex-direction: column;
        align-items: stretch;
        gap: 0.5rem;
    }
    .dialog-footer > * {
        margin: 0;
    }
    .dialog-footer .me-auto {
        margin-right: 0 !important;
        width: 100%;
    }
    .dialog-footer .me-auto > span {
        display: flex;
        width: 100%;
    }
    .dialog-footer .btn {
        width: 100%;
    }
}

/*
 * Job header action row on phones: the title takes the full row and the
 * actions get their own full row below it, sharing the width equally (like
 * four col-3: star, pin, delete, share) instead of keeping their minimized
 * square look. Extra stream controls wrap onto further equal-width rows.
 * Tablet and up keep the desktop look (buttons top-right, title shrinks).
 */
@media (max-width: 767.98px) {
    #job-detail-page .uf-page-head .ms-auto.job-actions {
        width: 100%;
        margin-left: 0 !important;
    }
    #job-detail-page .uf-page-head .ms-auto.job-actions .btn {
        flex: 1 1 0;
        width: auto;
        max-width: none;
        min-width: fit-content;
        justify-content: center;
    }
}

/* Offline-license status banner (licensed builds): the first in-flow element
   takes up its normal space on arrival, then remains reachable at the
   viewport edge after the user scrolls past it. Signal red with white text
   and action link. The icon rail is fixed-positioned (kit), so while the
   banner is present the rail — and the sticky filter panels hanging off the
   top bar — are offset below it by the measured banner height (set on
   :root by the inline script in base.html; 0 when the banner is absent). */
#license-status-banner {
    --bs-alert-color: #ffffff;
    --bs-alert-bg: #c62828;
    --bs-alert-border-color: #c62828;
    --bs-alert-link-color: #ffffff;
    position: sticky;
    top: 0;
    z-index: 1040;
    background-color: #c62828;
    color: #ffffff;
}

body.has-license-banner .uf-topbar {
    top: var(--license-banner-h, 0px);
}

body.has-license-banner .uf-rail {
    top: calc(var(--uf-topbar-h) + var(--license-banner-h, 0px));
}

body.has-license-banner .uf-panel {
    top: calc(var(--uf-topbar-h) + var(--license-banner-h, 0px) + 16px);
    max-height: calc(100vh - var(--uf-topbar-h) - var(--license-banner-h, 0px) - 32px);
}

#license-status-banner .alert-link,
#license-status-banner .alert-link:hover {
    color: #ffffff;
    text-decoration: underline;
}

/* Service-connectivity banner (database / MQTT / stacked queue): same
   full-width sticky pattern as the license banner above. Database faults
   are signal red; MQTT and queue-stack faults are a deep amber — both keep
   white text in light and dark mode so the banner stays prominent. The
   fixed topbar/rail/panels shift below it by the measured banner height
   (--service-banner-h on :root, set by the inline script in base.html). */
#service-status-banner {
    --bs-alert-color: #ffffff;
    --bs-alert-link-color: #ffffff;
    position: sticky;
    top: 0;
    z-index: 1040;
    border-radius: 0;
    background-color: #c62828;
    --bs-alert-bg: #c62828;
    --bs-alert-border-color: #c62828;
    color: #ffffff;
}

#service-status-banner[data-kind="mqtt"],
#service-status-banner[data-kind="queue"] {
    background-color: #b45309;
    --bs-alert-bg: #b45309;
    --bs-alert-border-color: #b45309;
}

#service-status-banner[data-kind="storage"] {
    background-color: #c62828;
    --bs-alert-bg: #c62828;
    --bs-alert-border-color: #c62828;
}

body.has-service-banner .uf-topbar {
    top: var(--service-banner-h, 0px);
}

body.has-service-banner .uf-rail {
    top: calc(var(--uf-topbar-h) + var(--service-banner-h, 0px));
}

body.has-service-banner .uf-panel {
    top: calc(var(--uf-topbar-h) + var(--service-banner-h, 0px) + 16px);
    max-height: calc(100vh - var(--uf-topbar-h) - var(--service-banner-h, 0px) - 32px);
}

#service-status-banner .alert-link,
#service-status-banner .alert-link:hover {
    color: #ffffff;
    text-decoration: underline;
}

/* Offline-license lock shell (licensed builds): locked non-root users get a
   full-page shell — base.html skips the icon rail markup, so the page area
   drops the rail offset here (mirrors the kit's below-desktop-breakpoint
   rule). Navigation survives in the top-bar user menu and the mobile
   drawer; sidebar.js is a no-op without its toggle button. */
body.license-locked .uf-shell {
    margin-left: 0;
}

/*
 * Worker detail fact list: identifiers are shown in full (never truncated
 * with an ellipsis) and wrap anywhere so long UUIDs/URLs stay inside the
 * column on narrow phones. Generous row spacing + relaxed line-height keep
 * the dense mono values readable.
 */
.vpp-id-full,
#worker-hero-job.vpp-chip-wrap {
    overflow-wrap: anywhere;
    word-break: break-word;
}
#worker-hero-job.vpp-chip-wrap {
    max-width: 100%;
    white-space: normal;
    text-align: left;
}
#worker-facts,
#worker-provision {
    row-gap: 0.65rem;
}
#worker-facts > dt,
#worker-facts > dd,
#worker-provision > dt,
#worker-provision > dd {
    margin-bottom: 0;
    line-height: 1.6;
}
#worker-signals-attempt {
    margin-top: 0.15rem;
}

/*
 * Worker detail copy rows: the value wraps (min-width: 0) while the copy
 * button stays a compact inline action next to the first text line.
 * The button itself is the kit's `.btn-icon btn-xs` (24px square, see
 * portal-theme-override.css) so it matches the fact-list line box and
 * does not stand taller than the text; the old custom `.vpp-copy-inline`
 * box (and the earlier `.btn-icon` at a fixed 2.25rem) both read as
 * misaligned against the mono value.
 */
.vpp-copy-row {
    display: flex;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 0.5rem;
    min-width: 0;
}
.vpp-copy-row .vpp-copy-text {
    flex: 0 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* Group settings use the Application Config subnav within a scrollable
   dialog; the navigation and Save/Cancel footer stay reachable. */
.dialog.group-settings-dialog {
    width: min(1080px, calc(100vw - 2rem));
    max-width: min(1080px, calc(100vw - 2rem));
}
.group-settings-dialog .dialog-body.group-settings-layout {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    padding: 0;
    overflow: hidden;
    min-height: 0;
}
.group-settings-dialog .group-settings-sidebar {
    width: auto;
    min-width: 0;
    padding: 16px;
    border-radius: 0;
    border-right: 1px solid var(--uf-border, var(--bs-border-color));
    overflow-y: auto;
}
.group-settings-content {
    min-width: 0;
    min-height: 0;
    overflow-y: auto;
    padding: 24px;
    overscroll-behavior: contain;
}
.group-settings-section + .group-settings-section {
    padding-top: 24px;
    margin-top: 24px;
    border-top: 1px solid var(--uf-border, var(--bs-border-color));
}
.group-settings-nav .uf-subnav-item { white-space: normal; }
.group-settings-mobile-nav { display: none; }
.group-settings-dialog .uf-choice-row { flex-wrap: wrap; }
.group-settings-dialog .uf-choice { flex: 1 1 150px; min-width: 0; }
@media (max-width: 767.98px) {
    .group-settings-dialog .dialog-body.group-settings-layout {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto minmax(0, 1fr);
    }
    .group-settings-dialog .group-settings-sidebar { border-right: 0; padding: 12px 16px; }
    .group-settings-nav { display: none; }
    .group-settings-mobile-nav { display: block; }
    .group-settings-content { padding: 16px; }
}

/* ============================================================================
   SmartWatch Portal additions (on top of the portal-template styles above)
   Page-specific rules for the SmartWatch business surfaces that the shared
   template does not style.
   ============================================================================ */

/* --- Audit "Content" cell: long JSON wraps rather than stretching the table */
#audit-tbody code {
    word-break: break-all;
    white-space: normal;
}

/* --- Device toolbar (dashboard) — bare flex row, no card wrapper -----------
   Search sits left; group select + refresh hug the right edge. */
.device-toolbar {
    display: flex;
    align-items: end;
    gap: 1rem;
    width: 100%;
}
.device-toolbar__search {
    flex: 1 1 36rem;
    max-width: 36rem;
}
.device-toolbar__controls {
    display: flex;
    align-items: end;
    gap: 1rem;
    flex: 0 0 auto;
    margin-left: auto;
}
.device-toolbar__group {
    flex: 0 0 12rem;
    max-width: 12rem;
}
.device-toolbar__sort {
    flex: 0 0 12rem;
    max-width: 12rem;
}

.device-toolbar__status {
    flex: 0 0 9rem;
    max-width: 9rem;
}

/* Batch-action button: lives in the toolbar controls beside the filters
   that define its target set. Bottom-aligned with the selects on desktop
   (the parent's align-items: end handles it); full-width second row on
   phones (the wrap below makes room for it). */
.device-toolbar__batch-btn {
    flex: 0 0 auto;
    align-self: end;
}

@media (max-width: 767.98px) {
    .device-toolbar {
        flex-direction: column;
        align-items: stretch;
    }
    .device-toolbar__search {
        flex-basis: auto;
        max-width: none;
        margin-left: 0;
    }
    .device-toolbar__controls {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: end;
        justify-content: space-between;
        gap: 1rem;
        width: 100%;
    }
    .device-toolbar__group,
    .device-toolbar__sort,
    .device-toolbar__status {
        flex: 1 1 auto;
        max-width: none;
        margin-left: 0;
    }
    .device-toolbar__batch-btn {
        flex: 1 1 100%;
    }
}

/* --- Leaflet map fix --------------------------------------------------------
   The kit's img reset collapses Leaflet's 256px tile images; restore the
   sizing Leaflet expects. */
.leaflet-container img { height: auto; max-width: none; }
.leaflet-container .leaflet-tile { width: 256px; height: 256px; }

/* --- Dashboard group combobox -------------------------------------------------
   Single-select group filter: a search input with a kit `.menu` dropdown
   listing All groups + every group (client-side search within the list).
   The menu is absolutely positioned under the input; `.show` drives
   visibility together with the `hidden` attribute (the attribute alone
   loses to the kit's layered `.menu.show` display rule if left set). */
#group-combobox .menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    max-height: 18rem;
    overflow-y: auto;
    padding: 0.25rem;
    z-index: 1050;
}
#group-combobox .menu-item {
    width: 100%;
    text-align: left;
    cursor: pointer;
}
#group-combobox .menu-item.is-active {
    font-weight: 600;
}

/* --- Group member table -------------------------------------------------------
   The device-name link reads as primary body text (white in dark mode,
   like the dashboard card title it mirrors) with the underline on hover
   only — not the dimmed default link color. */
.member-name-link {
    color: inherit;
    text-decoration: none;
}
.member-name-link:hover {
    color: inherit;
    text-decoration: underline;
}

/* --- Device status grid (device detail right rail) --------------------------
   Kit-token replacement for the muted definition-list labels. */
.device-detail-page .uf-kv { padding: 2px 0; }

/* --- Dashboard device cards -------------------------------------------------
   Hover affordance + selection ring for the selectable lock cards, and the
   fixed selection bar surface (the kit has no bg-white utility). */
.lock-card {
    cursor: pointer;
    transition: border-color 0.12s, box-shadow 0.12s;
}
.lock-card:hover { border-color: var(--uf-border-strong); }
.lock-card.selected {
    border-color: var(--uf-primary);
    box-shadow: var(--uf-ring);
}
.selection-bar { background: var(--uf-surface); }
/* Dashboard device cards: clean kit rows, values in tabular numerals.
   Cards are flat (no shadow). */
.device-card .device-row-val {
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    text-align: right;
    gap: 2px;
}
/* Per-row receive age below the value: muted, not important. */
/* Coordinates value links out to Google Maps in a new tab — keep the card's
   data styling but underline on hover so the click target is discoverable. */
.device-card .device-coord-link {
    color: inherit;
    /* Sit above the card's stretched-link overlay (::after, z-index 1) so
       the Maps link stays clickable while the rest of the card navigates. */
    position: relative;
    z-index: 2;
}
.device-card .device-coord-link:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
}
.device-card .device-row-age {
    display: block;
    font-weight: 400;
    line-height: 1.35;
}
.device-card .uf-kv { align-items: center; }
.device-card .uf-kv-key { display: inline-flex; align-items: center; gap: 8px; }
.device-card .uf-kv-key .lucide { width: 1rem; height: 1rem; color: var(--uf-text-2); }
/* Row directly above the footer drops its own border — the footer owns the
   single divider — and keeps an 8px gap above the sticky footer. */
.device-card .uf-kv:has(+ .device-card-foot) { border-bottom: 0; margin-bottom: 8px; }
/* "Last updated" footer: the full portal-format receive time below a
   hairline of its own, all muted small. The card body is a flex column
   and the footer pins itself with margin-top:auto, so it sticks to the
   bottom of every card even when rows above are short. The coordinates
   row keeps an 8px gap above the footer. */
.device-card-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    border-top: 1px solid var(--uf-border-light);
    margin-top: auto;
    padding-top: 8px;
    font-size: 0.75rem;
    color: var(--uf-text-3);
}
.device-card-foot > span:first-child { display: inline-flex; align-items: center; gap: 6px; }
.device-card-foot > span:last-child { text-align: right; }
.device-card-foot .lucide { width: 0.875rem; height: 0.875rem; }
/* Device card identity header: the status dot pins to the title's first
   line, so named cards keep the same dot alignment as SN-only cards, and
   long Sheet-synced names truncate with an ellipsis instead of stretching
   the card — the device-detail page still shows the full name and serial.
   Cards are flat (no shadow). */
.device-card { box-shadow: none; }
/* Defined card size: the body column stretches full height and the
   telemetry block grows to fill it, so cards in a row match heights
   and the sticky footer lands on the same baseline. Rows stay compact
   at the top — only the footer absorbs the slack. */
.device-card .card-body { height: 100%; }
.device-card .extra-info {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}
.device-card-identity { min-width: 0; }
.device-card-head .status-line { margin-top: 0.6rem; }
.device-card-head .device-card-title { min-width: 0; flex: 1 1 auto; }
/* Header icon actions (top-right, e.g. copy name/SN): a flex row reserved
   for future icon-only buttons. Sits above the stretched-link overlay
   (same z-index trick as the Coordinates link) so the buttons stay
   clickable while the rest of the card navigates. */
.device-card-actions {
    display: flex;
    align-items: center;
    gap: 2px;
    margin-left: auto;
    flex-shrink: 0;
    position: relative;
    z-index: 2;
}
.device-card-action-btn .lucide { width: 1rem; height: 1rem; }
.device-card-title,
.device-card-sn {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Serial / name stay selectable (highlight to copy) even though the
       card itself navigates on click — text cursor signals copyability. */
    user-select: text;
    cursor: text;
}

/* --- Device map layer switcher ----------------------------------------------
   Floating basemap picker over the map's top-right corner (Leaflet's zoom
   control owns the top-left). The anchor around the map box deliberately does
   not clip, so the kit menu can overhang the map edge. */
.map-layer-switch {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 1100; /* above Leaflet's control stack (z-index 1000) */
    /* Panel treatment (same as the fleet map's .map-source-switcher): the
       trigger is a transparent kit icon-button, so without a surface it
       reads as a bare glyph floating over the tiles. */
    background: var(--uf-surface, #fff);
    border: 1px solid var(--uf-border, rgba(0, 0, 0, 0.12));
    border-radius: 0.375rem;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    padding: 0.25rem;
}
.map-layer-switch .menu-item .map-layer-check {
    visibility: hidden;
}
.map-layer-switch .menu-item.active .map-layer-check {
    visibility: visible;
}
.map-layer-switch .menu {
    /* Twelve basemap entries: scroll inside the viewport instead of
       overhanging past the map's bottom edge on short screens. */
    max-height: min(70vh, 24rem);
    overflow-y: auto;
}

/* --- Location-history calendar stacking -------------------------------------
   The track card's Bootstrap 6 datepicker popup ([data-vc="calendar"],
   z-index 1000 via --bs-datepicker-zindex) must open above the floating
   basemap switcher (z-index 1100) on the same card. The popup is hoisted to
   the nearest scroll parent, so the override targets the popup itself with
   higher specificity than the vendor rule instead of relying on inheritance
   from the card. */
body [data-vc="calendar"] {
    --bs-datepicker-zindex: 1200;
}

/* --- Location-history calendar dots -----------------------------------------
   The track card's Bootstrap 6 Datepicker marks days that hold plottable
   GPS fixes (the /track-days feed) with a small dot under the day number.
   The day cell needs a positioning context; every Bootstrap 6 Datepicker
   renders its cells from the same [data-vc="calendar"] root, and
   position:relative is inert for pickers that carry no dot. */
[data-vc="calendar"] [data-vc-date] {
    position: relative;
}
.track-day-dot {
    position: absolute;
    left: 50%;
    bottom: 0.1875rem;
    width: 0.3125rem;
    height: 0.3125rem;
    transform: translateX(-50%);
    border-radius: 50%;
    background-color: #1a73e8; /* the track polyline blue */
    pointer-events: none;
}
/* Keep the dot visible over the filled selected-day background. */
[data-vc="calendar"] [data-vc-date-selected] .track-day-dot {
    background-color: var(--bs-datepicker-day-selected-color, #fff);
}

/* --- Fleet map page ----------------------------------------------------------
   The map page reflows the kit shell (map.html sets body.map-fullscreen):
   the footer is hidden and .uf-page becomes a flex column with no padding,
   so the map wrapper (.map-page-map, flex-grow-1) fills the entire page
   edge-to-edge. #fleet-map is absolutely inset within the positioned
   wrapper — the same concrete-height trick as the device-detail map, scaled
   up to the whole page. Everything else floats over the canvas. */
body.map-fullscreen .uf-shell > footer {
    display: none !important;
}
body.map-fullscreen .uf-page {
    display: flex;
    flex-direction: column;
    min-height: 0;      /* let the flex child shrink inside .uf-shell */
    padding: 0;         /* full bleed: no page padding on any side */
}

.map-page-map {
    min-height: 0;
    min-width: 0;
}
#fleet-map {
    position: absolute;
    inset: 0;
    z-index: 1; /* tiles below the floating controls (1200) and overlays */
}

/* Floating controls over the canvas: the dzassPortal-style basemap source
   switcher in the top-right corner (clear of Leaflet's zoom control on the
   top-left), the located-count badge in the bottom-left corner (clear of
   the attribution control on the bottom-right). Panels use kit
   surface/border tokens so they track the active light/dark theme. */
.map-source-switcher {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 1200;
    background: var(--uf-surface, #fff);
    border: 1px solid var(--uf-border, rgba(0, 0, 0, 0.12));
    border-radius: 0.375rem;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    padding: 0.25rem;
}
.map-source-switcher select.form-control {
    /* The select is the only child; let it own the panel width. */
    min-width: 10rem;
    border: 0;
}
.map-source-switcher select.form-control:focus {
    box-shadow: none;
}

.map-count-badge {
    position: absolute;
    left: 0.75rem;
    bottom: 1.25rem;   /* clears Leaflet's bottom-left empty corner + shadow */
    z-index: 1200;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.7rem;
    font-size: 0.8125rem;
    color: var(--uf-text-1, rgba(0, 0, 0, 0.87));
    background: var(--uf-surface, #fff);
    border: 1px solid var(--uf-border, rgba(0, 0, 0, 0.12));
    border-radius: 999px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    white-space: nowrap;
}

/* Loading/error/empty overlays: full-cover over the tile canvas so partial
   basemap paint never bleeds through a message. */
.map-overlay-text {
    position: absolute;
    inset: 0;
    z-index: 1100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    text-align: center;
    background: var(--uf-surface, #fff);
    color: var(--uf-text-2, #666);
}

/* Device popup: identity block, kit status badge, quiet label/value rows. */
.map-device-popup {
    min-width: 200px;
}
.map-device-popup .uf-badge {
    display: inline-flex;
}

/* Group detail map: concrete height inside the card (the fleet map's
   absolute-inset trick needs a full-bleed page, which a card is not).
   Shorter on phones where the card stacks full-width above the table. */
#group-map {
    height: 440px;
    z-index: 0;
    background: var(--uf-bg-muted, #eee);
}
@media (max-width: 991.98px) {
    #group-map {
        height: 300px;
    }
}

/* Uptime heatmap legend: not-yet-reached (future) hours render as diagonal
   hatching in the chart — the dot mirrors that, so future never reads as
   the solid-gray offline state. Kept to a faint border-tone stripe so the
   empty future stays quieter than the blue/green data cells. */
.uf-dot.is-future {
    background: repeating-linear-gradient(45deg, var(--uf-border, #e3e7ec) 0 1px, transparent 1px 4px);
    outline: 1px solid var(--uf-border, #e3e7ec);
    outline-offset: -1px;
}
