/* =========================================================================
   TransitLive Web — Map pane
   The pan/zoom stage, the app's static map backdrop, the SVG primitives
   ttc-map-render.js / gomap-render.js paint, the alert overlay layer, and
   the floating map chrome.

   Everything resolves through tokens.css. Nothing here hardcodes a colour,
   radius, duration or shadow that has a token.

   LAYER STACK inside .map-viewport
     z 0  .map-backdrop   view-space, NEVER transformed
     z 1  .map-root       the transformed layer (pan/zoom writes its transform)
     z 2  .map-chrome     floating controls, sibling of the viewport

   DOM CONTRACT (app.html)
     .map-viewport > .map-backdrop + .map-root > .map-svg
       > g#tracks-layer + g#alerts-layer + g#stations-layer
     .map-chrome > .map-chrome__left + .map-chrome__right > .glass-fab
   ========================================================================= */

/* =========================================================================
   1. VIEWPORT
   Full-bleed gesture surface. touch-action:none because the pan/zoom code
   owns every pointer — leaving the browser's default pan/pinch on would
   fight it and produce the rubber-banding the old mobile map suffered.
   ========================================================================= */
/* Shared vars live on the pane, the common ancestor of the viewport (which
   holds the backdrop) and the chrome (which is its sibling). */
.pane--map {
    /* Desktop vignette ink. Declared HERE, outside any media query, so the
       light/dark pair is always defined and only the @media consumes it. */
    --map-vignette: rgba(0, 0, 0, 0.30);

    /* Floating tab bar geometry, so the chrome can clear it without magic
       numbers: 2x --ds-space-6 inset + 2x --ds-space-12 tab padding + a
       20px icon = 56px, floating at --ds-safe-bottom + --ds-space-12. */
    --map-tabbar-h: 56px;
    --map-chrome-bottom: calc(
        var(--ds-safe-bottom) + var(--ds-space-12) + var(--map-tabbar-h) + var(--ds-space-22)
    );
}

body.light-mode .pane--map { --map-vignette: rgba(0, 0, 0, 0.10); }

.map-viewport {
    position: absolute;
    inset: 0;
    overflow: hidden;
    touch-action: none;
    background: var(--ds-bg-map);
    isolation: isolate;
    cursor: grab;
    -webkit-user-select: none;
    user-select: none;
}

.map-viewport.is-panning { cursor: grabbing; }

/* =========================================================================
   2. BACKDROP  (port of the app's static map ground)
   A dot grid plus a warm glow low-centre, where Union sits. Pure CSS
   gradients on one element — this replaces the Vanta three.js particle
   field, which cost a WebGL context and a rAF loop for a static look.

   CRITICAL: it lives OUTSIDE .map-root. It is VIEW space, not content
   space — the grid pitch must stay 26px on screen at every zoom level.
   Putting it inside the transformed layer makes the dots swell with the
   map and is the bug this file exists to avoid.
   ========================================================================= */
.map-backdrop {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* View-space dot grid ONLY. The Union glow used to live here too, which
       meant it stayed put while the network moved under it; it is now drawn in
       map space by map.js (addUnionGlow), matching the app. 44px matches
       TTCSchemaMapView.dotGridCanvas's 44pt spacing. */
    background-image: radial-gradient(
        circle at center,
        var(--ds-map-dotgrid) 0 1.5px,
        transparent 1.6px
    );
    background-size: 44px 44px;
    background-repeat: repeat;
}

/* =========================================================================
   3. TRANSFORMED LAYER
   ========================================================================= */
.map-root {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    touch-action: none;

    /* MUST be 0 0. Every fit/zoom-to-point calculation in map.js treats the
       layer's top-left as the transform anchor; the old site's
       `transform-origin: center` made every fly-to land off by half the
       viewport. Do not "tidy" this to `center`. */
    transform-origin: 0 0;

    /* Deliberately NO will-change/translateZ: promoting this layer rasterises
       it once and then scales the bitmap, which visibly blurs the map during
       a pinch. The old site hit exactly that and removed it. */
    backface-visibility: hidden;
}

.map-svg {
    display: block;
    width: 100%;
    height: 100%;
    /* map.js:applyViewBox() writes the viewBox from the schema's content
       bounds; .map-root supplies the pan/zoom transform on top of it.
       overflow:visible because terminus badges and labels legitimately
       spill the padded box, and .map-viewport does the real clipping. */
    overflow: visible;
    shape-rendering: geometricPrecision;
}

/* =========================================================================
   4. MAP PRIMITIVES
   Fills, stroke colours and font sizes are written as inline styles by
   ttc-map-render.js (schema-driven, so CSS must not fight them). What is
   left to CSS is joinery, hit behaviour and the label halo.
   ========================================================================= */
.track {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
    /* stroke + stroke-width come from the schema, inline. */
}

.station-marker {
    pointer-events: none;
}

.station-label {
    font-family: var(--ds-font-sans);
    fill: var(--ds-map-label);
    pointer-events: none;
    /* Halo: stroke painted UNDER the fill. Width is inline (0.14em of the
       label's own size) so it reads identically at every label tier. */
    paint-order: stroke;
    stroke: var(--ds-map-label-halo);
    stroke-linejoin: round;
}

.terminal-badge {
    pointer-events: none;
}

.terminal-text {
    font-family: var(--ds-font-rounded);
    font-variant-numeric: var(--ds-numeric);
    pointer-events: none;
}

/* Transparent tap targets, painted last by the renderer. */
.station-hit {
    cursor: pointer;
}

/* -------------------------------------------------------------------------
   4b. GO MARKERS
   gomap-render.js paints a different marker family from the TTC renderer
   (rounded capsule at a special interchange, ringed dot elsewhere) and these
   were previously styled in go-mobile.css, which app.html does not load — so
   without this block every GO marker fell back to the SVG default fill and
   the whole map rendered as black blobs.

   Labels are NOT here: GO labels use the shared .station-label above.

   Every pair below resolves through one token that already inverts between
   the themes, so there is no light-mode-only rule and no media query.
   `#ffffff` is the classic transit-map paper fill, deliberately fixed in both
   themes exactly as ttc-map-render.js draws it; it has no token.
   ------------------------------------------------------------------------- */
/* No .go-station-marker rule: the renderer sets cursor:pointer inline on the
   named stops only, so the anonymous shaping waypoints stay uninteractive. */

/* Special interchange. Dark: dark pill, light ring. Light: light pill, dark
   ring — one token flip, both directions. */
.go-capsule {
    fill: var(--ds-bg-secondary);
    stroke: var(--ds-map-label);
}

.go-capsule-accessible {
    fill: var(--ds-accessible-blue);
}

/* One dot per arrival slot inside the capsule; reads against the pill fill. */
.go-slot-circle {
    fill: var(--ds-map-label);
}

.go-interchange {
    fill: #ffffff;
    stroke: var(--ds-map-marker-outline);
}

/* Plain stop — the ring is the owning line's schema colour, set inline. */
.go-station-dot {
    fill: #ffffff;
}

.go-accessibility-icon {
    pointer-events: none;
    /* .map-svg asks for geometricPrecision; the nested glyph <svg> fragments
       on some engines at this scale unless it opts back out. */
    shape-rendering: auto;
}

/* Line-code pill past each terminus. The rect fill is the schema colour. */
.go-line-route-label {
    pointer-events: none;
}

.go-line-route-label-text {
    fill: #ffffff;
    paint-order: stroke;
    stroke: var(--ds-map-marker-outline);
    stroke-width: var(--ds-stroke-thin);
    stroke-linejoin: round;
}

/* High Visibility thickens map strokes, matching the app. 4px is
   gomap-render.js's MARKER_STROKE_WIDTH, which the renderer writes as a
   presentation attribute — so this rule wins without changing the base look.
   .go-station-dot is included because its ring carries the line colour and is
   the stroke that most needs the extra weight. */
body.high-contrast .go-capsule,
body.high-contrast .go-interchange,
body.high-contrast .go-station-dot {
    stroke-width: calc(4px * var(--ds-hv-stroke-scale, 1));
}

/* =========================================================================
   5. ALERT OVERLAY
   Renderer contract — classes it may put on a path in #alerts-layer:
     .alert-base            the severity capsule following the track sub-path
     .alert-base.delay      amber instead of red
     .alert-flow            marching dashes
       .is-reverse / .flow-reverse   flip direction (map.js emits the latter)
       .flow-both                    bidirectional: breathe instead of march
     .alert-pulse           breathing solid, standalone equivalent of flow-both
     .station-pulse         <g> holding .station-pulse__halo + __core
     .shuttle-outline       wide blue halo under the capsule
   .alert-flow / .alert-pulse / .shuttle-outline work either on their own
   path or combined onto the .alert-base element.

   KNOWN GAP (needs a map.js change, not a CSS one): map.js puts `delay` only
   on the .alert-base path. The sibling .alert-flow and .shuttle-outline paths
   therefore keep the red default, so an amber delay draws red marching pills.
   The one-line fix is to append the same `delay` suffix at map.js:203/215.

   GEOMETRY SCALING — the reason these numbers are not arbitrary.
   TTCMap.json draws in a ~3600 x 2750 unit space with a 38-unit track, so
   the stroke widths tuned for the old ~1300-unit hand-rolled map rendered
   as hairlines. These are the app's own TTCSchemaAlertLayer values: 32 for
   the capsule, 72 for the shuttle halo, 42/95 dashes. The GO schema is
   roughly a third that size and keeps its 22/32 and 20/45.

   style.css:236-268 scoped this to `body.ttc-mobile-page` because both maps
   shared one stylesheet. Here both maps share one <svg>, so the switch has to
   be a selector. TTC scale is the default, matching store.js.
   ========================================================================= */
.map-viewport {
    --map-alert-w: 32;
    --map-shuttle-w: 72;
    --map-dash: 42;
    --map-gap: 95;
    /* Only defined under body.high-contrast; 1 everywhere else. The app
       thickens map strokes x1.2 in High Visibility. */
    --map-hv: var(--ds-hv-stroke-scale, 1);
}

/* GO schema is roughly a third of TTC's unit space, so the TTC widths would
   render as blobs three times the width of a GO track.

   THE SWITCH IS NOT `#map-viewport[data-network]`. Nothing writes that
   attribute — shell.js:139 puts data-network on `.mode-picker` in the app bar,
   which is not an ancestor of the viewport, so the whole GO block used to be
   dead and every GO alert drew at TTC scale. Read the picker instead.

   Kept as two separate rules on purpose: `:has()` is a parse-time failure on
   older Safari, and one selector list would take the attribute rule down with
   it. The attribute rule stays valid so map.js can start writing the attribute
   later without another CSS change; both agree on the same network. */
.map-viewport[data-network="go"] {
    --map-alert-w: 22;
    --map-shuttle-w: 32;
    --map-dash: 20;
    --map-gap: 45;
}

body:has(.mode-picker[data-network="go"]) .map-viewport {
    --map-alert-w: 22;
    --map-shuttle-w: 32;
    --map-dash: 20;
    --map-gap: 45;
}

/* Severity hue as one local property, so nothing below has to win a
   specificity fight with `.delay`. */
.alert-base,
.alert-flow,
.alert-pulse,
.station-pulse {
    --map-alert-hue: var(--ds-alert-red);
}

.alert-base.delay,
.alert-flow.delay,
.alert-pulse.delay,
.station-pulse.delay {
    --map-alert-hue: var(--ds-alert-orange);
}

.alert-base,
.alert-flow,
.alert-pulse,
.shuttle-outline {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
    pointer-events: none;
}

/* --- the capsule ------------------------------------------------------- */
.alert-base {
    stroke: var(--map-alert-hue);
    stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1px);
    opacity: 0.9;
    /* Fallback first (no alpha), then the real thing. color-mix is inlined
       rather than hidden behind a custom property on purpose: a var() that
       fails at computed-value time would blank the declaration instead of
       falling back to the line above it. */
    filter: drop-shadow(0 0 calc(var(--ds-map-glow-pill) * 1px) var(--map-alert-hue));
    filter: drop-shadow(
        0 0 calc(var(--ds-map-glow-pill) * 1px)
        color-mix(in srgb, var(--map-alert-hue) calc(var(--ds-map-glow-alpha) * 100%), transparent)
    );
}

/* --- marching pills (directional alerts) ------------------------------- */
/* TTCSchemaAlertLayer draws directional alerts as pills ONLY — there is no
   solid capsule underneath them — so the pill carries the pure severity hue
   at full track width. (It used to be narrowed and lightened towards the
   label colour, which was the right call only while a solid base was being
   drawn beneath it; map.js no longer emits one.) */
.alert-flow {
    stroke: var(--map-alert-hue);
    stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1px);
    stroke-dasharray: calc(var(--map-dash) * 1px) calc(var(--map-gap) * 1px);
    animation: map-flow var(--ds-dur-alert-flow) linear infinite;
    filter: drop-shadow(0 0 calc(var(--ds-map-glow-pill) * 1px) var(--map-alert-hue));
    filter: drop-shadow(
        0 0 calc(var(--ds-map-glow-pill) * 1px)
        color-mix(in srgb, var(--map-alert-hue) calc(var(--ds-map-glow-alpha) * 100%), transparent)
    );
}

/* The dark separator the app draws behind every alert stroke, `separatorExtra`
   (8 schema units) wider on each side. Without it an orange delay on Line 1's
   yellow track is nearly invisible; this ring works on every combination.
   It shares .alert-flow's dash and animation so the two march in lockstep. */
.alert-sep {
    stroke: rgba(0, 0, 0, 0.55);
    stroke-width: calc((var(--map-alert-w) + 8) * var(--map-hv) * 1px);
    filter: none;
}

/* Legacy combined usage: the capsule itself marching. */
.alert-base.alert-flow,
.shuttle-outline.alert-flow {
    stroke: var(--map-alert-hue);
    stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1px);
}

/* map.js emits `flow-reverse`; `is-reverse` is honoured too because this
   file's contract block documents that name. */
.alert-flow.is-reverse,
.alert-flow.flow-reverse {
    animation-name: map-flow-reverse;
}

/* Bidirectional alerts are a solid capsule that breathes over the whole span,
   not a one-way march — that would be the wrong reading for an alert
   affecting both directions. */
.alert-flow.flow-both {
    stroke-dasharray: none;
    animation-name: map-breathe;
    animation-duration: var(--ds-dur-alert-pulse);
    animation-timing-function: var(--ds-ease-in-out);
}

/* The separator under a bidirectional capsule must not breathe independently
   of it, or its edge would peek out of phase. */
.alert-sep.flow-both { animation-name: map-breathe; }

@keyframes map-flow {
    from { stroke-dashoffset: 0; }
    to   { stroke-dashoffset: calc((var(--map-dash) + var(--map-gap)) * -1px); }
}

@keyframes map-flow-reverse {
    from { stroke-dashoffset: 0; }
    to   { stroke-dashoffset: calc((var(--map-dash) + var(--map-gap)) * 1px); }
}

/* --- breathing solid (bidirectional alerts) ---------------------------- */
.alert-pulse {
    stroke: var(--map-alert-hue);
    stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1px);
    stroke-dasharray: none;
    animation: map-breathe var(--ds-dur-alert-pulse) var(--ds-ease-in-out) infinite;
}

@keyframes map-breathe {
    0%, 100% { stroke-opacity: 0.6; stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1px); }
    50%      { stroke-opacity: 1;   stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1.18px); }
}

/* --- station ring ------------------------------------------------------ */
/* map.js:drawStationPulse emits a <g class="station-pulse"> holding
   .station-pulse__halo (r 26) and .station-pulse__core (r 13) — NOT the bare
   <circle> the contract block above described. Animating the group faded both
   children out together, so a single-station alert spent most of every cycle
   invisible and the two radii collapsed into one flat disc. The halo animates;
   the core is static so the stop always reads as affected.
   `fill` is inherited, so the hue set here reaches both children. */
.station-pulse {
    fill: var(--map-alert-hue);
    stroke: none;
    pointer-events: none;
}

/* Scales rather than animating `r`: SVG geometry properties are not
   animatable in Firefox, and the renderer already sizes the circle. */
.station-pulse__halo {
    transform-box: fill-box;
    transform-origin: center;
    animation: map-station-pulse var(--ds-dur-alert-pulse) var(--ds-ease-out) infinite;
}

.station-pulse__core {
    fill-opacity: 0.95;
}

@keyframes map-station-pulse {
    0%   { transform: scale(1);   fill-opacity: 0.55; }
    70%  { fill-opacity: 0; }
    100% { transform: scale(2.6); fill-opacity: 0; }
}

/* --- shuttle halo ------------------------------------------------------ */
.shuttle-outline {
    stroke: var(--ds-shuttle-blue);
    stroke-width: calc(var(--map-shuttle-w) * var(--map-hv) * 1px);
    filter: drop-shadow(0 0 calc(var(--ds-map-glow-bar) * 1px) var(--ds-shuttle-blue));
    filter: drop-shadow(
        0 0 calc(var(--ds-map-glow-bar) * 1px)
        color-mix(in srgb, var(--ds-shuttle-blue) calc(var(--ds-map-glow-alpha) * 100%), transparent)
    );
}

/* The halo keeps its own hue and width in every combination. */
.shuttle-outline.alert-flow,
.shuttle-outline.alert-pulse {
    stroke: var(--ds-shuttle-blue);
    stroke-width: calc(var(--map-shuttle-w) * var(--map-hv) * 1px);
}

.shuttle-outline.alert-pulse {
    animation-name: map-breathe-shuttle;
}

@keyframes map-breathe-shuttle {
    0%, 100% { stroke-opacity: 0.7; stroke-width: calc(var(--map-shuttle-w) * var(--map-hv) * 1px); }
    50%      { stroke-opacity: 1;   stroke-width: calc(var(--map-shuttle-w) * var(--map-hv) * 1.18px); }
}

/* Opt-in hairline between an alert capsule and the track beneath it. */
.alert-base.has-separator,
.shuttle-outline.has-separator {
    stroke: var(--ds-map-alert-separator);
}

/* =========================================================================
   6. FLOATING CHROME
   A full-bleed layer that must never eat a map drag: pointer-events:none on
   the layer and on the clusters, re-enabled only on the controls themselves
   so even the gap between two FABs stays draggable.
   ========================================================================= */
.map-chrome {
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    padding: 0 var(--ds-gutter-page) var(--map-chrome-bottom);
}

.map-chrome__left,
.map-chrome__right {
    pointer-events: none;
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-12);
}

.map-chrome__right { align-items: flex-end; }

/* Opt-in, not blanket: only real controls take pointer events. */
.map-chrome button,
.map-chrome .glass-fab,
.map-chrome .map-chrome__item {
    pointer-events: auto;
}

/* --- 44pt circular glass button ---------------------------------------- */
/* Same recipe as .glass-icon-btn in shell.css, at the minimum touch target. */
.glass-fab {
    width: var(--ds-hit-min);
    height: var(--ds-hit-min);
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    padding: 0;
    border-radius: 50%;
    border: var(--ds-stroke-hairline) solid var(--ds-hairline);
    background: var(--ds-glass-fill-material);
    -webkit-backdrop-filter: var(--ds-glass-blur);
    backdrop-filter: var(--ds-glass-blur);
    box-shadow: var(--ds-shadow-glass);
    color: var(--ds-label-primary);
    cursor: pointer;
    transition: transform var(--ds-dur-fast) var(--ds-ease-smooth),
                background var(--ds-dur-fast) var(--ds-ease-smooth),
                color var(--ds-dur-fast) var(--ds-ease-smooth);
}

.glass-fab:active { transform: scale(0.92); }

.glass-fab .icon { width: 20px; height: 20px; }

/* Sparse colour, per the Liquid Glass rules: tint only when a control is
   actually doing something (live location following, say). */
.glass-fab.is-on,
.glass-fab[aria-pressed="true"] { color: var(--ds-system-blue); }

.glass-fab:disabled {
    opacity: var(--ds-opacity-mode-unselected);
    cursor: default;
}

.glass-fab:focus-visible {
    outline: var(--ds-stroke-thin) solid var(--ds-system-blue);
    outline-offset: var(--ds-space-2);
}

/* =========================================================================
   7. BATTERY SAVER / REDUCED MOTION
   MUST include .alert-flow. tokens.css zeroes --ds-map-glow-alpha, which
   drains the shadow colour to transparent — but the `filter` property is
   still not `none`, so the element keeps its filter render surface and the
   compositor keeps running the drop-shadow pass on every repaint. Since
   .alert-flow is the only class map.js emits for a segment alert AND it
   animates stroke-dashoffset continuously, leaving it out meant the user
   lost the glow and kept nearly all of its cost.

   .alert-base and .alert-pulse are not listed: map.js never emits either.
   ========================================================================= */
body.battery-saver .alert-flow,
body.battery-saver .alert-sep,
body.battery-saver .shuttle-outline {
    filter: none;
}

/* Freeze, don't slow. stroke-dashoffset, stroke-opacity, stroke-width and
   fill-opacity are all main-thread paint properties, so a longer duration
   only shrinks the per-frame delta — the SVG still repaints on every
   displayed frame and the frame count is identical. Halting the animation is
   the only thing that actually removes work, and prefers-reduced-motion
   below already sets the precedent for frozen alert pills. */
body.battery-saver .alert-flow,
body.battery-saver .station-pulse__halo {
    animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
    /* Freeze the loops but keep the capsule visible — the alert must still
       read as an alert, it just stops moving. */
    .alert-flow,
    .alert-pulse,
    .shuttle-outline.alert-pulse {
        animation: none;
    }

    .station-pulse__halo {
        animation: none;
        fill-opacity: 0.45;
    }
}

/* =========================================================================
   8. DESKTOP
   The map is the permanent ground layer (shell.css keeps .pane--map active),
   so the chrome has to dodge two things: the bottom-left tab bar, and the
   right-hand list rail whenever a list tab is open.
   ========================================================================= */
@media (min-width: 900px) {
    .map-chrome {
        padding: 0 var(--ds-space-24) var(--ds-space-24);
    }

    /* The tab bar anchors bottom-left on desktop, so the left cluster stacks
       above it instead of beside it. */
    .map-chrome__left {
        margin-bottom: calc(var(--map-tabbar-h) + var(--ds-space-12));
    }

    /* Slide clear of the rail (.pane--list: min(440px, 38vw), inset 24px).
       :has() is the only way to see a sibling pane's state; browsers without
       it simply leave the chrome where it is, which is still usable. */
    .app-panes:has(.pane--list.is-active) .map-chrome__right {
        transform: translateX(calc(-1 * (min(440px, 38vw) + var(--ds-space-24))));
    }

    .map-chrome__right {
        transition: transform var(--ds-dur-select) var(--ds-ease-smooth);
    }

    /* Subtle vignette so the map reads as a surface under the floating rail
       rather than running flat to the window edge. */
    .map-backdrop::after {
        content: "";
        position: absolute;
        inset: 0;
        pointer-events: none;
        background: radial-gradient(
            ellipse 75% 70% at 50% 50%,
            transparent 55%,
            var(--map-vignette) 100%
        );
    }
}

/* =========================================================================
   TAP TARGETS
   The painted alert strokes stay pointer-events:none so they never eat a map
   drag; a separate invisible path carries the hit area. It lives in
   #alerts-layer, which paints below #stations-layer, so a station sitting on
   an affected segment still wins its own tap.
   ========================================================================= */
.alert-hit {
    stroke: transparent;
    /* Comfortably wider than the alert capsule so it is tappable on a phone
       without zooming, in schema units (the whole map scales together). */
    stroke-width: calc((var(--map-alert-w) + 34) * 1px);
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
    pointer-events: stroke;
    cursor: pointer;
}

circle.alert-hit { pointer-events: all; }

/* =========================================================================
   FOCUS  ("Show on map")
   Moving the camera is not enough on its own — with eleven alerts on screen
   the user cannot tell which one they asked for. When a focus is active every
   other alert drops back so the focused one reads immediately.
   ========================================================================= */
#alerts-layer.has-focus .alert-flow:not(.is-focused),
#alerts-layer.has-focus .alert-sep:not(.is-focused),
#alerts-layer.has-focus .shuttle-outline:not(.is-focused),
#alerts-layer.has-focus .station-pulse:not(.is-focused) {
    opacity: 0.22;
    filter: none;                 /* drop the glow too, or dimming barely shows */
    transition: opacity var(--ds-dur-select) var(--ds-ease-smooth);
}

/* Slightly heavier than its unfocused self so it reads as "this one".
   Scoped to .alert-flow: an unscoped .is-focused also matches the shuttle
   halo, which would collapse its 72-unit width down to the 32-unit alert
   width and undo the whole point of the halo. */
#alerts-layer.has-focus .alert-flow.is-focused {
    stroke-width: calc(var(--map-alert-w) * var(--map-hv) * 1.12px);
}

#alerts-layer.has-focus .alert-flow.alert-sep.is-focused {
    stroke-width: calc((var(--map-alert-w) * 1.12 + 8) * var(--map-hv) * 1px);
}

#alerts-layer.has-focus .shuttle-outline.is-focused {
    stroke-width: calc(var(--map-shuttle-w) * var(--map-hv) * 1.06px);
}

/* The bottom banner is anchored bottom-LEFT, which is exactly where the info
   FAB lives. Lift the left cluster above it while a banner is actually filled
   — ads.js sets .has-ad-banner on <body>. The right cluster needs no offset:
   the banner's width is capped to stop short of it.

   margin-bottom, not bottom: .map-chrome is a flex row with align-items
   flex-end, so these clusters are flex items with no box offsets to set. */
body[data-tab="map"].has-ad-banner .map-chrome__left {
    margin-bottom: calc(50px + var(--ds-space-10));
}
