/* Page content to full width */

a.custom-link {
    color: #007BFF;       /* Bootstrap blue */
    font-weight: bold;    /* Bold text */
    text-decoration: none; /* Remove underline */
}

a.custom-link:hover {
    color: #0056b3;       /* Darker blue on hover */
}
.page-content {
    max-width: none;
    width: 100%;
    padding: 112px 20px 0;
}

/* Every box below is measured border-box, and this says so rather than assuming
   it. On the public map that was true only by accident: base.html loads Tailwind
   v2, whose preflight sets `*, ::before, ::after { box-sizing: border-box }` for
   the whole document. A saved map (/map/<group>/<slug>/) renders the same markup
   and the same stylesheet inside main.html, which has no such reset — so the
   identical rules drew different boxes there. A `.sensor-card` sits in a
   `minmax(220px, 1fr)` grid track, and under the default content-box its 15px
   padding and 1px border landed OUTSIDE that track: the cards overflowed their
   cells, `.card-readings { width: 100% }` overflowed the card in turn, the
   nowrap number columns spilled past the edge, and `.map-panel__head`
   (`width: 100%` + 10px padding) overflowed the layers panel that clips it.

   Declared here, scoped to the map's own subtrees, so any page that includes
   this stylesheet gets the geometry it was written for. On the public page it is
   the value Tailwind already set — a no-op there, a fix everywhere else. */
.map-container, .map-container *, .map-container *::before, .map-container *::after,
#plotOverlay, #plotOverlay *, #plotOverlay *::before, #plotOverlay *::after {
    box-sizing: border-box;
}

.map-container { display: flex; flex-wrap: wrap; gap: 20px; position: sticky; top: 0; }
#map { height: 80vh; flex: 2; min-width: 300px; border-radius: var(--r-sm); }
#sensor-dashboard {
    flex: 1; min-width: 300px; height: 80vh; overflow-y: auto;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 15px; align-content: start;
    /* Momentum scrolling, and a scrollbar that is actually visible. A nested
       scroll region with an overlay scrollbar gives a touch user no signal that
       the area scrolls at all, and nothing to grab — which is what made this
       hard to move through on a phone. */
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;                       /* Firefox */
    scrollbar-color: #94a3b8 rgba(0, 0, 0, 0.06);
}
/* WebKit/Blink: declaring a width opts out of the overlay scrollbar, so the bar
   stays on screen instead of fading after each gesture. */
#sensor-dashboard::-webkit-scrollbar { width: 10px; }
#sensor-dashboard::-webkit-scrollbar-track {
    background: rgba(0, 0, 0, 0.06); border-radius: var(--r-sm);
}
#sensor-dashboard::-webkit-scrollbar-thumb {
    background: #94a3b8; border-radius: var(--r-sm);
    border: 2px solid transparent; background-clip: content-box;
}
#sensor-dashboard::-webkit-scrollbar-thumb:hover { background: #64748b; }

.dark-mode #sensor-dashboard { scrollbar-color: #64748b rgba(255, 255, 255, 0.08); }
.dark-mode #sensor-dashboard::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.08); }
.dark-mode #sensor-dashboard::-webkit-scrollbar-thumb { background: #64748b; }

/* Keep Leaflet top controls clear of the fixed navbar. */

@media (max-width: 768px) {
    .page-content {
        padding-top: 92px;
    }

    /* The flex row wraps on a phone, so `80vh` map + `80vh` dashboard stacked
       to 160vh of nested scrolling — most of it off-screen, with the page's own
       scroll fighting the list's. Shorter panes put the map and the top of the
       list on one screen and leave the page itself easy to move through.
       `sticky` is dropped for the same reason: pinning a 160vh block is what
       made the page feel stuck. */
    .map-container { position: static; }
    #map { height: 45vh; }
    #sensor-dashboard { height: 50vh; }
    /* Wider bar on touch: 10px is a comfortable mouse target and a poor thumb
       one. */
    #sensor-dashboard::-webkit-scrollbar { width: 14px; }
}

.sensor-card { background: var(--surface-2); border: 1px solid #ddd; border-radius: var(--r-sm); padding: 15px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: all 0.2s ease-in-out; }
/* `font-weight: inherit` is the same reason as the box-sizing block above: on
   the public page Tailwind's preflight already neutralises the UA's bold <h5>,
   so the card title has always rendered at body weight there. Stating it keeps
   the title identical on a shell that has no preflight, instead of bold on one
   page and not the other. */
.sensor-card h5 { margin-top: 0; margin-bottom: 10px; font-size: 1rem; font-weight: inherit; }
.sensor-card ul { list-style: none; padding: 0; margin: 0; font-size: 0.9rem; }
.sensor-card li { padding: 4px 0; }

/* Readings table: name | now | 24h average.
   `width: 100%` with `table-layout: auto` is the point of the change — the card
   is a grid cell of at least 220px and the old "Label: value" line left most of
   that empty on every breakpoint, mobile included. Two value columns spend the
   width that was already being paid for. */
.card-readings { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
.card-readings th, .card-readings td { padding: 3px 0; vertical-align: baseline; }
.card-readings thead th {
    font-size: var(--fs-0); text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--ink-3); font-weight: 600; text-align: right; padding-bottom: 4px;
    border-bottom: 1px solid var(--line);
}
.card-readings thead th:first-child { text-align: left; }
.card-readings tbody th {
    text-align: left; font-weight: 500; padding-right: 8px;
    /* The label is the widest and least precise column; let it absorb the
       leftovers so the two numeric columns stay narrow and aligned. */
    width: 100%;
}
/* Numbers are compared down a column, so right-align and use tabular figures —
   proportional digits make 1.11 look narrower than 8.88 and defeat the scan. */
.card-readings td { text-align: right; white-space: nowrap; font-family: var(--font-data); font-variant-numeric: tabular-nums; font-size: 0.94em; }
.card-readings .cr-avg { color: var(--ink-3); padding-left: 10px; }
/* Desktop packs 3-4 cards onto a row, so each is narrower than the single
   full-width card a phone gets — the label column absorbs the slack and the two
   numeric columns end up nearly touching. Phones already read well, so the
   extra separation is added only where the crowding happens. */
@media (min-width: 641px) {
    .card-readings .cr-avg { padding-left: 24px; }
    .card-readings thead th + th { padding-left: 24px; }
}

/* The card is a button when it can move the map (see focusSensorOnMap). */
.sensor-card.is-clickable { cursor: pointer; }
.sensor-card.is-clickable:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: 2px; }

.dark-mode .card-readings thead th { color: var(--ink-3); border-bottom-color: var(--line); }
.dark-mode .card-readings .cr-avg { color: var(--ink-3); }
.sensor-card .timestamp { font-size: 0.75rem; color: var(--ink-3); margin-top: 10px; }
.sensor-card:hover { transform: translateY(-3px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }

/* Sensor name labels */
/* Sensor name labels.
 *
 * There used to be two rules for this: this one, setting `background:
 * transparent`, and a `.sensor-label` rule further down giving it a solid white
 * box. The second never applied — `.leaflet-tooltip.sensor-label` is two
 * classes to its one, and specificity beats source order — so the labels have
 * only ever had a text-shadow to separate them from the map, which is not
 * enough over satellite tiles or a dark basemap. Both are now this one rule.
 *
 * Frosted rather than solid: a semi-transparent fill with `backdrop-filter`
 * lifts the text off the tiles while still letting the map read through, so a
 * dense cluster of labels does not turn into a wall of white boxes. Where
 * `backdrop-filter` is unsupported the fill alone still carries it — it
 * degrades to a plain translucent pill, not to unreadable text. */
.leaflet-tooltip.sensor-label {
    background: var(--sensor-label-fill, rgba(255, 255, 255, 0.55));
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    border: none;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
    border-radius: var(--r-xs);
    padding: 1px 6px;
    color: var(--ink);
    font-weight: 600;
    font-size: 12px;
    white-space: nowrap;
    /* Kept, and deliberately light: it is the fallback that keeps the text
       legible in the moment before the backdrop paints, and on the engines that
       never paint it at all. */
    text-shadow: var(--sensor-label-halo, 0 1px 2px rgba(255, 255, 255, 0.7));
}

/* The fill is the one thing above that did NOT flip with the theme, and it is
   the only one that had to: `color: var(--ink)` goes to #F2F2EF in dark mode
   while the pill stayed a 55% WHITE, which over a light basemap tile paints a
   near-white pill for near-white text. Measured on the live public map
   (/map/demo/putah-creek/, dark): rendered pill rgb(247,246,243) under ink
   #F2F2EF is 1.04:1 — the labels are gone, not dim.
 *
 * The dark fill is DEEPER than the light one, and that is arithmetic rather
 * than taste. The label sits on whatever tile is under it, so the worst case
 * is the brightest tile the basemap can serve. For near-white ink to clear
 * 4.5:1 against #141413 composited over a pure white tile the fill needs
 * alpha >= 0.61; 0.72 is that with headroom (6.6:1 over white, more over
 * anything darker). Light mode keeps 0.55 because its worst case is the
 * opposite tile and already measures 6.2:1 there.
 *
 * `body.dark-mode` and not a `prefers-color-scheme` media query: dark mode in
 * this app is a CLASS both shells stamp on <body> before the first paint, from
 * an explicit stored choice when there is one and from the OS preference when
 * there is not. Keying off the class is what covers all three states —
 * chose-dark, chose-light, and never-chose — with one rule. Same argument as
 * the .gs-navlogo block in pages/group_site.css. */
body.dark-mode .leaflet-tooltip.sensor-label {
    --sensor-label-fill: rgba(20, 20, 19, 0.72);
    --sensor-label-halo: 0 1px 2px rgba(20, 20, 19, 0.7);
}

/* ---------------------------------------------------------------------- */
/* Layers + legend panel (one control, top right)                          */
/*                                                                         */
/* Replaces the pair it used to be — Leaflet's layer control top right and  */
/* a legend bottom right. Every row is a toggle AND its key, so the swatch  */
/* sits against the checkbox that governs it.                              */
/* ---------------------------------------------------------------------- */
.map-panel {
    background: white;
    border-radius: var(--r-sm);
    box-shadow: 0 1px 5px rgba(0,0,0,0.35);
    font-size: 0.78rem;
    line-height: 1.3;
    min-width: 11rem;
    max-width: 15rem;
    overflow: hidden;
}
.map-panel__head {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    width: 100%;
    padding: 7px 10px;
    border: 0; background: none;
    font: inherit; font-weight: 600; color: var(--ink-2);
    text-align: left; cursor: pointer;
}
.map-panel__head:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: -3px; }
.map-panel__chev {
    width: 0; height: 0; flex: none;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
    transition: transform .15s ease;
}
.map-panel.is-collapsed .map-panel__chev { transform: rotate(-90deg); }
.map-panel.is-collapsed .map-panel__body { display: none; }

.map-panel__body { padding: 0 10px 9px; }
.map-panel__subhead {
    font-size: var(--fs-0); text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--ink-3); margin: 7px 0 3px;
}
/* The whole row is the label, so the swatch and the name are click targets
   for the checkbox as well — a 12px checkbox is a small thing to hit. */
.map-panel__row {
    display: flex; align-items: center; gap: 7px;
    margin: 1px 0; padding: 2px 0;
    cursor: pointer;
}
.map-panel__row input { flex: none; margin: 0; cursor: pointer; }
.map-panel__swatch {
    display: inline-flex; width: 16px; justify-content: center; flex: none;
}
.map-panel__name { overflow-wrap: anywhere; }
/* Sensor sources: a key under the Sensors checkbox, indented past it so it
   reads as an explanation of that row rather than four more toggles. */
.map-panel__sources { margin: 1px 0 2px 22px; }
.map-panel__key {
    display: flex; align-items: center; gap: 7px;
    margin: 2px 0; color: var(--ink-2); font-size: 0.72rem;
}
.map-panel__sources:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
    .map-panel__chev { transition: none; }
}

.legend-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.6); }
.legend-line {
    height: 5px;
    width: 18px;
    display: inline-block;
    vertical-align: middle;
}
/* Dark mode */
.dark-mode .sensor-card { background: #2a3b4a; border-color: var(--line); color: var(--ink); }
.dark-mode .sensor-card .timestamp { color: var(--ink-3); }
.dark-mode .map-panel { background: #2a3b4a; color: var(--ink); border: 1px solid var(--line); }
.dark-mode .map-panel__head { color: var(--ink); }
.dark-mode .map-panel__subhead { color: var(--ink-3); }
.dark-mode .map-panel__key { color: #c3ccd5; }


/* Footer */
.site-footer { background: var(--surface-2); padding: 24px 0; margin-top: 24px; text-align: center; border-top: 1px solid var(--line); }
.footer-logos { display: flex; justify-content: center; align-items: center; gap: 32px; margin-bottom: 12px; }
.footer-logos img { max-height: 40px; width: auto; }
.site-footer p { margin: 0; color: var(--ink-3); font-size: 14px; }

.dark-mode .site-footer { background: var(--surface); border-top-color: var(--line); }
.dark-mode .site-footer p { color: #ddd; }

/* No arrow. Leaflet draws one as a bordered triangle, which cannot be given a
   backdrop-filter and so renders as a hard opaque spike hanging off a frosted
   pill. The label sits directly above its marker; nothing needs pointing at.
   (These were four `!important` rules painting the arrow black against a
   background that was transparent at the time — a stray dark tick over the
   map.) */
.leaflet-tooltip.sensor-label::before { display: none !important; }

/* All sensor nodes */
.map-sensor-node, .legend-dot.hydraulic-crosshair {
    background-color: #2e7d32;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 1.5px solid white;
    box-shadow: 0 0 4px rgba(0,0,0,0.6);
    position: relative;
    display: block;
}

/* Hydraulic sensor: horizontal crosshair ray */
.map-sensor-node.is-hydraulic::before, .legend-dot.hydraulic-crosshair::before {
    content: "";
    position: absolute;
    top: 50%; 
    left: 0; 
    right: 0; 
    height: 1.5px;
    background-color: var(--surface);
    transform: translateY(-50%);
}

/* Hydraulic sensor: vertical crosshair ray */
.map-sensor-node.is-hydraulic::after, .legend-dot.hydraulic-crosshair::after {
    content: "";
    position: absolute;
    left: 50%; 
    top: 0; 
    bottom: 0; 
    width: 1.5px;
    background-color: var(--surface);
    transform: translateX(-50%);
}

/* Storm drain legend. No margin of its own — the panel row centres it in a
   fixed-width swatch slot and spaces it with `gap`. */
.legend-dot.storm-drain {
    background-color: #e76f51;
    border: 0.5px solid white !important;
    width: 8px;
    height: 8px;
    margin: 0;
}

/* ---------------------------------------------------------------------- */
/* Per-sensor plot window (opened by clicking a marker)                    */
/* ---------------------------------------------------------------------- */

/* The backdrop is the click-to-dismiss surface AND the centring layer.
   z-index sits above Leaflet's own panes: its controls and tooltips run up to
   z-index 1000, so anything lower would put map furniture on top of the
   dialog. */
.plot-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgba(15, 23, 42, 0.55);
}
/* [hidden] needs the explicit none: the display:flex above would otherwise win
   the cascade and the dialog would render while still marked hidden. */
.plot-overlay[hidden] { display: none; }

.plot-window {
    background: var(--surface);
    border-radius: var(--r-md);
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
    width: min(900px, 100%);
    /* Bounded height with the body scrolling inside, so a sensor with many
       variables cannot grow the dialog past the viewport and push its own
       close button off-screen. */
    max-height: min(88vh, 900px);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.plot-window__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 16px 20px 12px;
    border-bottom: 1px solid var(--line);
    flex: none;
}

.plot-window__title {
    margin: 0;
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--ink);
    /* A long site name truncates rather than wrapping the close button away. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.plot-window__close {
    flex: none;
    width: 36px;
    height: 36px;
    border: 0;
    border-radius: var(--r-sm);
    background: var(--surface-2);
    color: var(--ink-2);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color .12s ease, color .12s ease;
}
.plot-window__close:hover { background: var(--surface-2); color: var(--ink); }
.plot-window__close:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: 2px; }

.plot-window__meta {
    margin: 0;
    padding: 10px 20px 0;
    font-size: 0.78rem;
    color: var(--ink-3);
    flex: none;
}

/* ---------------------------------------------------------------------- */
/* A SCROLL REGION THAT SAYS SO — `.nh-scrollcue`                          */
/*                                                                        */
/* WHY THIS EXISTS. Two overlays on the open map hide more than half of   */
/* themselves. Measured 2026-09-04 at 1440x900: the layers panel's body    */
/* is 540px tall over 1168px of content, and the Flood answer card's body  */
/* is 589px over 1147px — and in the flood card the hidden half is the     */
/* CHART, the entire output of the button that was pressed. Both regions   */
/* already had `overflow-y: auto`, so both genuinely scrolled. What they   */
/* had no way of saying is THAT they scroll: a modern browser paints an    */
/* overlay scrollbar that fades within a second of the last gesture, and   */
/* on a touch device it is never drawn until a finger moves. A reader who  */
/* does not know there is more below does not go looking for it, which is  */
/* indistinguishable from the content not being there.                     */
/*                                                                        */
/* The cue is the classic "scrolling shadows" technique and it is used     */
/* here because it needs NO JavaScript and cannot fall out of step with    */
/* the scroll position: two opaque covers painted with                     */
/* `background-attachment: local` travel WITH the content and slide away   */
/* at each end, and two shadows painted with `background-attachment:       */
/* scroll` stay pinned to the box. The result is a shadow at the top only  */
/* while there is content above, and at the bottom only while there is     */
/* content below — the shadow disappears exactly when the reader has       */
/* reached the end, so it never lies about there being more.               */
/*                                                                        */
/* `--nh-cue-surface` MUST be the region's own opaque background or the    */
/* covers do not cover; every caller sets it, and dark mode re-sets it.    */
/* The scrollbar is additionally asked to stay drawn (`scrollbar-width:    */
/* thin` plus an explicit WebKit width opts out of the overlay bar) for    */
/* the same reason `#sensor-dashboard` above does it: a nested scroll      */
/* region with an invisible bar gives a touch reader nothing to grab.      */
/* ---------------------------------------------------------------------- */
.nh-scrollcue, .plot-window__body {
    --nh-cue-surface: var(--surface, #fff);
    --nh-cue-shadow: rgba(15, 23, 42, 0.20);
    background-image:
        linear-gradient(var(--nh-cue-surface) 40%, transparent),
        linear-gradient(transparent, var(--nh-cue-surface) 60%),
        radial-gradient(farthest-side at 50% 0, var(--nh-cue-shadow), transparent),
        radial-gradient(farthest-side at 50% 100%, var(--nh-cue-shadow), transparent);
    background-repeat: no-repeat;
    background-size: 100% 26px, 100% 26px, 100% 11px, 100% 11px;
    background-position: 0 0, 0 100%, 0 0, 0 100%;
    background-attachment: local, local, scroll, scroll;
    scrollbar-width: thin;
    scrollbar-color: #94a3b8 rgba(0, 0, 0, 0.08);
}
.nh-scrollcue::-webkit-scrollbar, .plot-window__body::-webkit-scrollbar { width: 10px; }
.nh-scrollcue::-webkit-scrollbar-track, .plot-window__body::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.06); }
.nh-scrollcue::-webkit-scrollbar-thumb, .plot-window__body::-webkit-scrollbar-thumb {
    background: #94a3b8; border-radius: var(--r-sm);
    border: 2px solid transparent; background-clip: content-box;
}
.dark-mode .nh-scrollcue, .dark-mode .plot-window__body {
    --nh-cue-shadow: rgba(0, 0, 0, 0.55);
    scrollbar-color: #64748b rgba(255, 255, 255, 0.08);
}
.dark-mode .nh-scrollcue::-webkit-scrollbar-track, .dark-mode .plot-window__body::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.08); }
.dark-mode .nh-scrollcue::-webkit-scrollbar-thumb, .dark-mode .plot-window__body::-webkit-scrollbar-thumb { background: #64748b; }
@media (prefers-reduced-motion: reduce) {
    /* Nothing animates here; the block is left in place so a future hover
       transition on the bar cannot be added without seeing this. */
    .nh-scrollcue, .plot-window__body { scroll-behavior: auto; }
}

.plot-window__body {
    padding: 12px 20px 20px;
    overflow-y: auto;
    flex: 1 1 auto;
}

.plot-window__chart { width: 100%; min-height: 260px; margin-top: 12px; }
.plot-window__msg { padding: 28px 0; text-align: center; color: var(--ink-3); font-size: 0.9rem; }
/* The numbers under a chart, on a page with no sensor cards to carry them
   (map_dashboard.js, `cardless`). Tabular figures so latest/low/high/mean line
   up as digits rather than drifting with the glyph widths. */
.plot-window__summary {
    margin: 4px 0 0;
    font-size: 0.78rem;
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
}

/* On a phone the dialog is the whole screen: a centred card with margins wastes
   the width these charts need most. */
@media (max-width: 640px) {
    .plot-overlay { padding: 0; }
    .plot-window { width: 100%; max-height: 100%; height: 100%; border-radius: 0; }
}

.dark-mode .plot-window { background: var(--surface); }
.dark-mode .plot-window__head { border-bottom-color: #444; }
.dark-mode .plot-window__title { color: var(--ink); }
.dark-mode .plot-window__close { background: var(--surface-2); color: var(--ink); }
.dark-mode .plot-window__close:hover { background: #375a9e; color: #fff; }
.dark-mode .plot-window__meta, .dark-mode .plot-window__msg,
.dark-mode .plot-window__summary { color: var(--ink-3); }


/* ---------------------------------------------------------------------- */
/* "It's this one" — the marker ping fired by clicking a card              */
/*                                                                        */
/* Flying to a sensor answers "where is it?" but not "which pin?" when     */
/* several sit close together, so the marker announces itself for a couple */
/* of seconds. A ring drawn in ::after rather than on the marker itself,   */
/* so the icon's own artwork is untouched and nothing shifts layout.       */
/* ---------------------------------------------------------------------- */
.sensor-ping { position: relative; z-index: 1000; }
.sensor-ping::after {
    content: "";
    position: absolute;
    left: 50%; top: 50%;
    width: 18px; height: 18px;
    margin: -9px 0 0 -9px;
    border-radius: 50%;
    border: 3px solid var(--brand-gold);
    pointer-events: none;          /* never intercept a click meant for the pin */
    animation: sensor-ping-ring 1.1s ease-out 2;
}
@keyframes sensor-ping-ring {
    0%   { transform: scale(1);   opacity: 0.9; }
    100% { transform: scale(4.5); opacity: 0; }
}

/* Respect a reduced-motion preference: keep the identification, drop the
   movement. A steady ring still says "this one" without animating. */
@media (prefers-reduced-motion: reduce) {
    .sensor-ping::after { animation: none; opacity: 0.9; transform: scale(2); }
}

/* ---------------------------------------------------------------------- */
/* Right-click-to-copy confirmation                                        */
/* ---------------------------------------------------------------------- */
.map-container { position: relative; }   /* anchor for the toast below */

.map-toast {
    position: absolute;
    left: 50%; top: 16px;
    transform: translate(-50%, -8px);
    z-index: 1200;                       /* above Leaflet controls (max ~1000) */
    background: rgba(15, 23, 42, 0.92);
    color: #fff;
    padding: 8px 14px;
    border-radius: var(--r-sm);
    font-size: 0.82rem;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
    pointer-events: none;                /* never block a click on the map */
    opacity: 0;
    transition: opacity .18s ease, transform .18s ease;
}
.map-toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.map-toast.is-error { background: rgba(153, 27, 27, 0.94); }

@media (prefers-reduced-motion: reduce) {
    .map-toast { transition: none; }
}

/* ---------------------------------------------------------------------- */
/* Go-to-coordinates control (bottom left)                                 */
/* ---------------------------------------------------------------------- */
/* No panel: the control is just its input and button. A white card here read as
   a second, heavier UI element in the corner of the map, and it only existed to
   hold a label and a hint that the placeholder already says. The input carries
   its own background, which is what it actually needs to stay legible over
   tiles. */
.coord-search {
    background: none;
    box-shadow: none;
    padding: 0;
    margin: 0 0 10px 10px;   /* matches Leaflet's own control spacing */
}
/* The visible label is gone; kept for screen readers, which otherwise meet an
   unlabelled text box. */
.coord-search__label {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.coord-search__row { display: flex; gap: 4px; }
.coord-search__input {
    width: 11.5rem; max-width: 42vw;
    padding: 4px 7px;
    border: 1px solid rgba(0, 0, 0, 0.18); border-radius: var(--r-xs);
    background: rgba(255, 255, 255, 0.94);
    font-size: 0.74rem; font-variant-numeric: tabular-nums;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
}
.coord-search__input:focus { outline: 2px solid var(--accent-ring); outline-offset: 1px; }
.coord-search__go {
    padding: 4px 9px; border: 0; border-radius: var(--r-xs);
    background: var(--accent); color: #fff; font-size: 0.74rem; font-weight: 600;
    cursor: pointer; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
}
.coord-search__go:hover { background: var(--accent); }
.coord-search__go:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: 2px; }
/* The hint only appears when the input is REJECTED — as a standing line it was
   another row of chrome saying what the placeholder already shows. */
.coord-search__hint { display: none; }
.coord-search__hint.is-error {
    display: block; font-size: var(--fs-0); color: #fff; margin-top: 3px;
    background: rgba(153, 27, 27, 0.92); padding: 3px 6px; border-radius: var(--r-xs);
    max-width: 15rem;
}

@media (max-width: 640px) {
    .coord-search__input { width: 8.5rem; }
}

.dark-mode .coord-search__input {
    background: rgba(31, 44, 56, 0.94); border-color: var(--line); color: var(--ink);
}

/* ---------------------------------------------------------------------- */
/* Sensor freshness dot (card title)                                       */
/*                                                                        */
/* Levels mirror SensorDetails.ingest_health, so the dot, the sensors-page */
/* badge and the staleness alarm cannot disagree.                          */
/*                                                                        */
/* Colour is not the only signal: each level also has a distinct ring, so  */
/* a red/green colour-blind viewer can still tell them apart, and the span */
/* carries an aria-label with the level's name.                            */
/* ---------------------------------------------------------------------- */
/* Sized against the .timestamp line (0.75rem) it now sits on, not the card
   title it used to — a 10px dot beside 12px text reads as a bullet. */
.status-dot {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    margin-right: 6px;
    vertical-align: baseline;
    flex-shrink: 0;
}
.status-dot--healthy { background: var(--good); box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.22); }
.status-dot--stale   { background: var(--critical); box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.22); }
.status-dot--warning { background: #d97706; box-shadow: 0 0 0 2px rgba(217, 119, 6, 0.22); }
/* Hollow, so "we don't know" never reads as a quiet pass. */
.status-dot--unknown { background: transparent; border: 2px solid var(--ink-3); }

.dark-mode .status-dot--healthy { background: var(--good); }
.dark-mode .status-dot--stale   { background: #f87171; }
.dark-mode .status-dot--warning { background: #fbbf24; }
.dark-mode .status-dot--unknown { border-color: var(--ink-3); }

/* ---------------------------------------------------------------------- */
/* Head band: title, standing description and tips, on one row             */
/*                                                                        */
/* All three used to be stacked — a full-width <h1>, then the map, then    */
/* the source note and the tips underneath. The page is ~1300px wide and   */
/* prose only wants ~60ch of that, so the words now sit BESIDE the title   */
/* rather than above and below the map. The map moves up, the text that    */
/* explains it is on screen at the same time as the thing it explains, and */
/* it costs about the vertical space the heading alone already took.       */
/*                                                                        */
/* Bottom alignment, not baseline: the band's job is to fill the strip     */
/* between the navbar and the first pixel of map, and what a reader wants  */
/* nearest the map is the words about it — so the prose is flushed DOWN to */
/* the band's floor and the last line of the note lands on the row         */
/* immediately above the map. Baseline alignment hung the note from the    */
/* title's baseline instead, which pinned it to the TOP of the band and    */
/* left the gap underneath it, furthest from the thing it describes and    */
/* growing wider the taller the title wrapped.                             */
/* ---------------------------------------------------------------------- */
.map-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 4px 28px;
    /* Close to the map on purpose. The band describes the thing directly
       underneath it, so the two read as one unit; 12px here was a visible
       channel between a caption and what it captions. */
    margin: 0 0 6px;
}
.map-head__title { margin: 0; flex: 0 1 auto; line-height: 1.15; }
/* Everything that is not the title takes the rest of the row and splits it
   again, so the description and the tips are two readable columns rather
   than one very long measure across the whole page. The 300px basis is what
   decides when they stop sharing a line: below ~630px of text width they
   stack, and below that the whole band stacks under the title. */
/* …and the same alignment one level down, which is what keeps the tips
   dropdown level with the note rather than level with the note's FIRST
   line. The two columns are different heights and both are fluid — the
   note rewraps with the viewport, the dropdown changes height when it is
   opened — so anything that aligns their tops drifts apart as either one
   reflows. Aligning their floors is what makes "next to the notes" hold
   at every width and in both open and closed states. */
.map-head__text {
    flex: 1 1 420px;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 6px 28px;
    min-width: 0;
}
.map-head__note {
    flex: 1 1 300px;
    margin: 0;
    max-width: 66ch;
    font-size: 0.82rem;
    line-height: 1.5;
    color: var(--ink-3);
}
.map-head__note a { font-weight: 600; }
/* In the band the tips are a column: one gesture per line, which keeps the
   block to three short lines instead of wrapping mid-sentence across the
   width the description is already using. */
.map-head .map-tips {
    flex: 1 1 300px;
    margin: 0;
    flex-direction: column;
    gap: 4px;
}
.dark-mode .map-head__note { color: var(--ink-3); }

/* ---------------------------------------------------------------------- */
/* "How to use this map"                                                   */
/*                                                                        */
/* Quiet by design: these are hints for a first visit, not controls, and   */
/* they should not compete with the map itself.                            */
/* ---------------------------------------------------------------------- */
.map-tips {
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 8px 22px;
    font-size: 0.82rem;
    color: var(--ink-3);
}
.map-tips li { display: flex; align-items: baseline; gap: 7px; }
.map-tips li::before {
    content: "";
    flex: none;
    width: 5px; height: 5px;
    border-radius: 50%;
    background: #2887b1;
    transform: translateY(-2px);
}
.map-tips strong { color: var(--ink-2); font-weight: 600; }

/* One per line on a phone: three of these side by side wrap into an
   unreadable block at narrow widths. */
@media (max-width: 640px) {
    .map-tips { flex-direction: column; gap: 6px; }
}

.dark-mode .map-tips { color: var(--ink-3); }
.dark-mode .map-tips strong { color: var(--ink); }
