/* Lock the page to the viewport — every scrollable region (sidebar, main,
   side panels) handles its own overflow internally, so the outer document
   never shows a scrollbar. Default body margin would otherwise push 100vh
   layouts past the viewport edge. */
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }

h1:focus {
    outline: none;
}

/* Clicking a KMZ or flight line focuses its SVG <path>, and a browser draws the
   focus ring around a path's BOUNDING BOX rather than around the stroke — so
   clicking a survey line that runs twenty miles north puts a black rectangle
   across half the map. It reads as a selection artifact, and it is: the line
   answers a click with the info card on the map pages, and the open line is
   redrawn heavier (see selectImportedFeature in leaflet-interop.js), so the ring
   carries nothing the page isn't already showing. The map CONTAINER's own focus
   ring is untouched — that one marks real keyboard navigation.

   Deliberately broad (the <svg> and anything inside it) rather than `path` alone:
   which element ends up focused differs by browser, and every one of them is a
   Leaflet-drawn vector carrying no tabindex, so none is keyboard-reachable and
   none loses a real focus indicator here. */
.leaflet-container svg:focus,
.leaflet-container svg *:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.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;
}

/* Live Map — helicopter marker + system-name label */
.heli-icon {
    background: transparent;
    border: none;
    filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.35));
}
/* Inner wrapper the interop rotates to the GPS heading — Leaflet owns the
   icon root's transform for positioning, so rotation can't live there. */
.heli-icon .veh-rot {
    width: 100%;
    height: 100%;
    transition: transform 0.5s ease;
    will-change: transform;
}
.heli-icon .veh-rot svg { display: block; }
.leaflet-tooltip.heli-label {
    background: rgba(15, 23, 42, 0.85);
    color: #f8fafc;
    border: none;
    border-radius: 6px;
    padding: 2px 8px;
    font-weight: 600;
    font-size: 0.8rem;
    letter-spacing: 0.02em;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.leaflet-tooltip.heli-label.heli-label--online {
    background: #bbf7d0;
    color: #0f172a;
}
.leaflet-tooltip.heli-label.heli-label--offline {
    background: #fef08a;
    color: #0f172a;
}
.leaflet-tooltip.heli-label::before { display: none; }

/* Live Map — marker popup: Leaflet forces white on .leaflet-popup-content-wrapper and .leaflet-popup-tip */
.leaflet-popup:has(.heli-popup--online) .leaflet-popup-content-wrapper,
.leaflet-popup:has(.heli-popup--online) .leaflet-popup-tip {
    background: #bbf7d0 !important;
}
.leaflet-popup:has(.heli-popup--offline) .leaflet-popup-content-wrapper,
.leaflet-popup:has(.heli-popup--offline) .leaflet-popup-tip {
    background: #fef08a !important;
}
.leaflet-popup-content:has(.heli-popup-inner) {
    margin: 0 !important;
}

/* Per-feed status banner — shown on each system subpage (DAS, POS, Power, Cameras, Drives). */
.feed-status-banner {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .55rem .9rem;
    border-radius: 8px;
    margin: 0 0 .85rem 0;
    font-size: .9rem;
    line-height: 1.3;
}
.feed-status-banner.is-online {
    background: #ecfdf5;
    color: #065f46;
    border: 1px solid #a7f3d0;
}
.feed-status-banner.is-offline {
    background: #fef3c7;
    color: #78350f;
    border: 1px solid #fcd34d;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
.feed-status-banner.is-waiting {
    background: #f1f5f9;
    color: #475569;
    border: 1px solid #cbd5e1;
}
.feed-status-banner .feed-status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex: 0 0 auto;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, .65);
}
.feed-status-banner .feed-status-dot.is-online { background: #10b981; }
.feed-status-banner .feed-status-dot.is-offline { background: #d97706; }
.feed-status-banner .feed-status-dot.is-waiting { background: #94a3b8; }
.feed-status-banner.is-offline .feed-status-dot {
    animation: feed-status-pulse 1.6s ease-in-out infinite;
}
@keyframes feed-status-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}
.feed-status-banner .feed-status-text { flex: 1; }

/* Cellular signal icon (SignalBars control) — rising bars + a network-generation
   tag (5G/4G). Global (not CSS-isolated) to match the other shared controls. */
.sigbar { display: inline-flex; align-items: center; gap: .28rem; vertical-align: middle; line-height: 1; }
.sigbar-track { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; }
.sigbar.lg .sigbar-track { height: 30px; gap: 3px; }
.sigbar-bar { width: 5px; background: #e2e8f0; border-radius: 1px; }
.sigbar.lg .sigbar-bar { width: 8px; border-radius: 2px; }
.sigbar-bar.lvl1 { height: 35%; }
.sigbar-bar.lvl2 { height: 55%; }
.sigbar-bar.lvl3 { height: 78%; }
.sigbar-bar.lvl4 { height: 100%; }
.sigbar-bar.on { background: #22c55e; }
.sigbar-track.weak .sigbar-bar.on { background: #f59e0b; }
.sigbar-track.bad .sigbar-bar.on { background: #ef4444; }
.sigbar-net { font-size: .62rem; font-weight: 700; letter-spacing: .02em; color: #475569; }
.sigbar.lg .sigbar-net { font-size: .8rem; }
/* Ortho coverage mask — the raster of ground that was inside at least one frame,
   drawn as a Leaflet image overlay (leafletInterop.addCoverageMask). Nearest-neighbour
   scaling is deliberate: the browser would otherwise smooth the edge of the swath into
   a soft gradient, and the edge is exactly what an operator reads to decide whether the
   ground beside a line was covered. Global rather than page-scoped because the element
   is created by Leaflet inside the map pane, where CSS isolation does not reach. */
.coverage-mask { image-rendering: pixelated; image-rendering: crisp-edges; }
/* Live Map weather box — Radar / Rain / Wind gusts drawn over the basemap from OpenWeather
   (leafletInterop.addWeatherBox). Global rather than page-scoped for the same reason
   .coverage-mask is: Leaflet builds the control inside the map container, where Blazor's
   CSS isolation does not reach. */
/* Sized and shaped against .leaflet-control-layers, not .leaflet-bar, so the cloud icon and
   the basemap-stack icon are the same square: 36px, and 44px once Leaflet decides the device
   is touch and adds .leaflet-touch. The numbers are copied rather than the class reused, so
   the layers control's own rules cannot reach in here. */
.weather-box { background: #fff; border-radius: 5px; box-shadow: 0 1px 5px rgba(0,0,0,.4); }
.weather-box-toggle { position: relative; display: block; width: 36px; height: 36px; border-radius: 5px; background-position: 50% 50%; background-repeat: no-repeat; background-size: 26px 26px; background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2026%2026'%3E%3Cg%20fill='%23333333'%3E%3Ccircle%20cx='10'%20cy='10.5'%20r='4.6'/%3E%3Ccircle%20cx='16.4'%20cy='12.1'%20r='3.6'/%3E%3Crect%20x='6'%20y='11.4'%20width='13.2'%20height='4.3'%20rx='2.15'/%3E%3C/g%3E%3Cg%20stroke='%23333333'%20stroke-width='1.7'%20stroke-linecap='round'%3E%3Cpath%20d='M9.2%2018.2L8%2020.8'/%3E%3Cpath%20d='M13.1%2018.2L11.9%2020.8'/%3E%3Cpath%20d='M17%2018.2L15.8%2020.8'/%3E%3C/g%3E%3C/svg%3E"); }
.leaflet-touch .weather-box-toggle { width: 44px; height: 44px; }
/* Leaflet swaps the shadow for a border on touch, and only for its own two control
   classes. Without the same swap here the weather square renders 44px beside a 48px
   layers square, which is the whole reason the two icons looked mismatched. */
.leaflet-touch .weather-box { box-shadow: none; border: 2px solid rgba(0,0,0,.2); background-clip: padding-box; }
.weather-box-toggle:hover { background-color: #f4f4f4; }
/* A layer is drawing. Without this the only evidence of what is switched on is the weather
   itself, which is the thing the operator is trying to read. */
.weather-box-toggle.is-active { background-color: #eef2ff; }
/* Something failed. The panel is closed most of the time, so this dot is what stops a dead
   layer from passing as clear skies until somebody happens to hover. */
.weather-box-toggle.has-issue::after { content: ""; position: absolute; top: 5px; right: 5px; width: 7px; height: 7px; border-radius: 50%; background: #dc2626; box-shadow: 0 0 0 2px #fff; }
/* Hover swaps the icon for the panel, exactly as the layers control does. */
.weather-box-body { display: none; width: 15rem; padding: .5rem .6rem .55rem; font-size: .8rem; color: #334155; }
.weather-box.is-expanded .weather-box-toggle { display: none; }
.weather-box.is-expanded .weather-box-body { display: block; }
.weather-box-head { margin-bottom: .35rem; padding-bottom: .3rem; border-bottom: 1px solid #e2e8f0; color: #0f172a; }
.weather-row { display: flex; align-items: flex-start; gap: .45rem; padding: .22rem 0; cursor: pointer; }
.weather-row:has(.weather-row-check:disabled) { cursor: default; opacity: .55; }
.weather-row-check { margin-top: .18rem; flex: 0 0 auto; cursor: pointer; }
.weather-row-text { display: flex; flex-direction: column; min-width: 0; }
.weather-row-name { color: #0f172a; font-weight: 600; }
.weather-row-hint { color: #64748b; font-size: .7rem; line-height: 1.25; }
/* The gust reading, which is a different measurement from the raster above it and reads as
   one only if it is set apart from the rows. */
.weather-gusts { margin: .35rem 0 .1rem; padding: .3rem .4rem; background: #f1f5f9; border-radius: 4px; color: #0f172a; font-size: .74rem; line-height: 1.3; font-variant-numeric: tabular-nums; }
/* The particles sub-toggle sits under the row it qualifies, indented to read as part of it
   without pretending to be a fourth layer. */
.weather-row--sub { margin-left: 1.15rem; padding-top: 0; }
/* Wind is not the chosen layer, so its overlay has nothing to sit on. */
.weather-row--sub.is-unavailable { opacity: .45; cursor: default; }
.weather-row--sub .weather-row-name { font-weight: 500; }
/* How many samples the direction field rests on. Not decoration: the particles interpolate
   just as smoothly over two readings as over twenty. */
.weather-note { margin: .1rem 0 .1rem 1.15rem; color: #64748b; font-size: .68rem; line-height: 1.3; font-variant-numeric: tabular-nums; }
/* The speed ramp the particles are coloured by. */
.wind-legend { margin: .3rem 0 .15rem 1.15rem; }
.wind-legend-bar { display: flex; height: 6px; border-radius: 3px; border: 1px solid rgba(15,23,42,.15); overflow: hidden; }
.wind-legend-cell { flex: 1; }
/* Six equal grid columns under six equal swatches: a left-aligned label in column N sits
   exactly on swatch N's left edge, i.e. on the bin boundary it names. */
.wind-legend-scale { display: grid; grid-template-columns: repeat(6, 1fr); margin-top: .1rem; color: #64748b; font-size: .62rem; font-variant-numeric: tabular-nums; }
.wind-legend-scale span { white-space: nowrap; }
/* Wind under the pointer, after a second of stillness. Never a click target itself — it sits
   over the map and would otherwise swallow the clicks meant for whatever is under it. */
.wind-hover-chip { position: absolute; z-index: 1000; pointer-events: none; background: rgba(15,23,42,.92); color: #f8fafc; border-radius: 5px; padding: .3rem .45rem; font-size: .74rem; line-height: 1.25; box-shadow: 0 2px 8px rgba(15,23,42,.35); white-space: nowrap; }
.wind-hover-main { font-weight: 700; font-variant-numeric: tabular-nums; }
.wind-hover-sub { color: #cbd5e1; font-size: .66rem; }
/* Colour separation for the rasters, which OpenWeather ships deliberately subtle. This cannot
   raise a tile's baked-in alpha — that is why wind carries its intensity in the particles
   instead — but it does pull rain apart from the ground it falls on. */
.leaflet-weatherParticles-pane { pointer-events: none; }
.leaflet-weather-pane { filter: saturate(1.7) contrast(1.15); }
/* Only while something is drawn over it, so a map with no weather on looks untouched. */
.leaflet-container.weather-active .leaflet-tile-pane { filter: grayscale(.6) contrast(.92) brightness(1.04); }
.weather-opacity { display: flex; align-items: center; gap: .4rem; margin-top: .45rem; padding-top: .4rem; border-top: 1px solid #e2e8f0; }
.weather-opacity-label { color: #64748b; font-size: .72rem; }
.weather-opacity-range { flex: 1; min-width: 0; cursor: pointer; }
.weather-opacity-value { flex: 0 0 auto; width: 2.4rem; text-align: right; color: #475569; font-size: .72rem; font-variant-numeric: tabular-nums; }
/* Red, and never display:none while it holds text: every way this box can fail ends here,
   because a weather layer drawing nothing is indistinguishable from clear skies. */
.weather-status { margin-top: .4rem; color: #b91c1c; font-size: .72rem; line-height: 1.3; }
.weather-credit { margin-top: .35rem; color: #94a3b8; font-size: .66rem; }


/* ---- One run's account of itself, as chips ------------------------------------------------
   Shared by FootprintSetStats, SwathRunStats and ProbeRunStats, and site-wide rather than
   scoped to each of them BECAUSE they are three components: Blazor rewrites a .razor.css
   selector with its own component's attribute, so a scoped copy reaches only its own markup
   and the vocabulary had to be repeated verbatim three times. These rules say how a sensor
   reports what its run was short of, and three sensors drifting into three visual languages
   would read as three different kinds of claim. One copy is the only way that cannot happen. */
.fp-cam-stats {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    align-items: center;
    margin-top: .3rem;
    font-size: .75rem;
    color: #475569;
}
/* Every way a run can be less than it looks gets a chip rather than a silence. The warn tint is
   reserved for the ones that change what the picture MEANS — a refused swath drew nothing at all,
   a capped set stops dead mid-pass, an unoriented frame drew nothing, and a reading that could not
   be placed is one this layer will never show. A plain chip is a figure worth reading, not a
   fault. */
.fp-chip {
    border: 1px solid #cbd5e1;
    border-radius: 999px;
    padding: 0 .4rem;
    font-size: .7rem;
    white-space: nowrap;
}
.fp-chip.is-warn { border-color: #fca5a5; background: #fef2f2; color: #b91c1c; }
.fp-cam-note { margin: .2rem 0 0; font-size: .72rem; color: #64748b; line-height: 1.35; }

/* The note downloads.js places under a Download scene / Download camera button when the
   download cannot start. Positioned in page coordinates, so it stays beside its row when the
   tree scrolls. Click to dismiss; it also goes by itself after twelve seconds. */
.cc-download-note {
    position: absolute;
    z-index: 1100;
    max-width: 360px;
    padding: .45rem .65rem;
    border-radius: 6px;
    background: #0f172a;
    color: #f8fafc;
    font-size: .82rem;
    line-height: 1.35;
    box-shadow: 0 4px 14px rgba(15, 23, 42, .25);
    cursor: pointer;
}
