:root{
  /* SFO livery: the airport's cyan on airport navy. --accent-soft is the lift
     used for hovers, glows and the flash when a number changes. */
  --bg:#0a1420; --panel:rgba(15,26,41,.9); --edge:rgba(110,150,190,.2);
  --ink:#e8eff8; --dim:#9fb3c9; --faint:#6a8098;
  --accent:#00a9ce; --accent-soft:#4fd3ee; --accent-rgb:0,169,206;
  --warn:#ffb648; --alert:#ff5f6d; --ok:#43d6a0; --purple:#c58bff;
  --shadow:0 14px 44px rgba(0,0,0,.6);
  --ease:cubic-bezier(.22,.61,.36,1);
  --r:14px;
  /* Fluid type: readable on a phone, legible across an office from a TV.
     Everything below is expressed against these, so one clamp scales the lot. */
  --fs:clamp(13px, 0.62vw + 9px, 26px);
  --fs-sm:calc(var(--fs) * .84);
  --fs-xs:calc(var(--fs) * .74);
  --fs-lg:calc(var(--fs) * 1.24);
  --fs-xl:calc(var(--fs) * 1.9);
  --pad:clamp(10px, .7vw, 22px);
  --panel-w:clamp(300px, 25vw, 520px);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0; background:var(--bg); color:var(--ink); font-size:var(--fs);
  font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif;
  line-height:1.45; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility; overflow:hidden;
}
/* Google Maps forcibly sets position:relative on its own container. This page
   used to size #map with position:fixed + inset:0, so that override collapsed
   the map to zero height, no tiles were ever requested, and the engine fell
   back to OSM. The wrapper holds the geometry now and #map simply fills it,
   which leaves Google free to style its container however it likes. */
#mapwrap{position:fixed; inset:0; background:var(--bg)}
#map{width:100%; height:100%; background:var(--bg)}
h1,h2{margin:0; font-weight:650; letter-spacing:-.01em}
a{color:var(--accent)}

/* ---------- top bar ---------- */
.bar{
  position:fixed; z-index:600; top:0; left:0; right:0;
  display:flex; align-items:center; gap:calc(var(--pad)*1.4); flex-wrap:wrap;
  padding:var(--pad) calc(var(--pad)*1.3); padding-top:max(var(--pad),env(safe-area-inset-top));
  background:linear-gradient(180deg,rgba(8,11,20,.95),rgba(8,11,20,.72) 72%,transparent);
  backdrop-filter:blur(10px); pointer-events:none;
}
.bar>*{pointer-events:auto}
.brand{display:flex; align-items:center; gap:.6em}
.brand h1{font-size:var(--fs-lg)}
.brand h1 span{color:var(--accent)}
.dot{width:.62em; height:.62em; border-radius:50%; background:var(--faint); flex:none}
.dot.on{background:var(--ok); animation:pulse 2.6s ease-out infinite}
.dot.stale{background:var(--warn)} .dot.off{background:var(--alert)}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(67,214,160,.55)}70%{box-shadow:0 0 0 .8em rgba(67,214,160,0)}100%{box-shadow:0 0 0 0 rgba(67,214,160,0)}}
@media (prefers-reduced-motion:reduce){.dot.on{animation:none}}

.stats{display:flex; gap:.55em; flex-wrap:wrap; margin-left:auto}
.stat{
  display:flex; flex-direction:column; align-items:center; min-width:4.4em;
  padding:.32em .7em; border:1px solid var(--edge); border-radius:.7em; background:var(--panel);
}
.stat b{font-size:var(--fs-lg); font-variant-numeric:tabular-nums; line-height:1.1}
.stat span{font-size:var(--fs-xs); color:var(--dim); text-transform:uppercase; letter-spacing:.07em}
.stat.warn b{color:var(--warn)} .stat.alert b{color:var(--alert)}

.meta{display:flex; align-items:center; gap:.8em; color:var(--dim); font-size:var(--fs-sm)}
.clock{font-variant-numeric:tabular-nums; font-size:var(--fs-lg); color:var(--ink); font-weight:600}
#updated{font-variant-numeric:tabular-nums; white-space:nowrap}
.ghost{
  background:var(--panel); color:var(--ink); border:1px solid var(--edge);
  border-radius:.55em; padding:.35em .8em; font:inherit; font-size:var(--fs-sm); cursor:pointer;
}
.ghost:hover{border-color:var(--accent); color:var(--accent)}
.ghost:focus-visible,input:focus-visible,.tab:focus-visible{outline:2px solid var(--accent); outline-offset:2px}

/* ---------- side panel ---------- */
.panel{
  position:fixed; z-index:600; left:var(--pad); top:calc(var(--fs) * 4.4);
  bottom:calc(var(--pad) + var(--band-h,0px)); width:var(--panel-w);
  display:flex; flex-direction:column; gap:calc(var(--pad)*1.1);
  overflow-y:auto; overscroll-behavior:contain;
  padding:calc(var(--pad)*1.1); border:1px solid var(--edge); border-radius:var(--r);
  background:var(--panel); backdrop-filter:blur(14px); box-shadow:var(--shadow);
  scrollbar-width:thin; scrollbar-color:var(--faint) transparent;
}
.panel.hide{display:none}
.panel h2{
  font-size:var(--fs-xs); text-transform:uppercase; letter-spacing:.1em;
  color:var(--dim); margin-bottom:.55em; display:flex; justify-content:space-between; align-items:center;
}
.count{color:var(--faint); font-size:var(--fs-xs); letter-spacing:0; text-transform:none}
.panel section{flex:none}
.caveat{margin:.5em 0 0; font-size:var(--fs-xs); color:var(--faint); line-height:1.4}

/* ---------- freeway condition board ---------- */
.roadgrid{list-style:none; margin:0; padding:0; display:grid; gap:.4em;
  grid-template-columns:repeat(auto-fill,minmax(6.6em,1fr))}
.roadgrid li{
  padding:.45em .55em; border-radius:.55em; border:1px solid var(--edge);
  border-left-width:.28em; background:rgba(255,255,255,.03); cursor:pointer;
}
.roadgrid li:hover{background:rgba(var(--accent-rgb),.12)}
.roadgrid li.clear{border-left-color:var(--ok); opacity:.62}
.roadgrid li.minor{border-left-color:var(--faint)}
.roadgrid li.moderate{border-left-color:var(--warn)}
.roadgrid li.major{border-left-color:var(--alert); background:rgba(255,95,109,.1)}
.roadgrid .rt{display:block; font-weight:700; font-size:var(--fs-sm); font-variant-numeric:tabular-nums}
.roadgrid .rs{display:block; font-size:var(--fs-xs); color:var(--dim); margin-top:.1em}
.roadgrid .empty{grid-column:1/-1; color:var(--faint); text-align:center; padding:1em; border:0; background:none; cursor:default}

/* ---------- feed ---------- */
.feed{flex:1 1 auto; display:flex; flex-direction:column; min-height:9em}
.tabs{display:flex; gap:.35em; margin-bottom:.55em}
.tab{
  flex:1; padding:.35em .6em; font:inherit; font-size:var(--fs-xs); cursor:pointer;
  color:var(--dim); background:transparent; border:1px solid var(--edge); border-radius:.5em;
}
.tab.on{color:var(--accent); border-color:var(--accent); background:rgba(var(--accent-rgb),.14)}
.feed-list{list-style:none; margin:0; padding:0; overflow-y:auto; flex:1;
  scrollbar-width:thin; scrollbar-color:var(--faint) transparent}
.feed-list li{
  padding:.5em .6em; margin-bottom:.35em; border-radius:.55em; cursor:pointer;
  border:1px solid var(--edge); border-left-width:.24em; background:rgba(255,255,255,.02);
}
.feed-list li:hover{background:rgba(var(--accent-rgb),.11); border-color:var(--accent)}
.feed-list li.major{border-left-color:var(--alert)}
.feed-list li.moderate{border-left-color:var(--warn)}
.feed-list li.minor{border-left-color:var(--faint)}
.feed-list .t{font-size:var(--fs-sm); font-weight:600; display:block}
.feed-list .l{font-size:var(--fs-xs); color:var(--dim); display:block; margin-top:.1em}
.feed-list .x{font-size:var(--fs-xs); color:var(--faint); margin-top:.15em; display:block}
.feed-list .empty{color:var(--faint); text-align:center; padding:1.2em; border:0; background:none; cursor:default}

.layerwrap{display:grid; grid-template-columns:1fr 1fr; gap:.15em .6em}
.layers label{display:flex; align-items:center; gap:.5em; padding:.22em 0; cursor:pointer; font-size:var(--fs-xs)}
.layers input{position:absolute; opacity:0; width:0; height:0}
.sw{width:2.1em; height:1.15em; border-radius:.6em; background:#26314c; border:1px solid var(--edge);
  position:relative; transition:background .18s; flex:none}
.sw::after{content:""; position:absolute; top:.13em; left:.13em; width:.78em; height:.78em;
  border-radius:50%; background:var(--faint); transition:transform .18s,background .18s}
.layers input:checked + .sw{background:rgba(var(--accent-rgb),.34); border-color:var(--accent)}
.layers input:checked + .sw::after{transform:translateX(.92em); background:var(--accent)}

.credits{color:var(--faint); font-size:var(--fs-xs); border-top:1px solid var(--edge);
  padding-top:.7em; margin-top:auto; line-height:1.4}
.credits p{margin:0 0 .35em}

/* ---------- rotating alert band ---------- */
.band{
  position:fixed; z-index:640; left:var(--pad); right:var(--pad); bottom:var(--pad);
  display:flex; align-items:center; gap:calc(var(--pad)*1.2);
  padding:calc(var(--pad)*.9) calc(var(--pad)*1.2);
  border:1px solid var(--edge); border-radius:var(--r);
  background:var(--panel); backdrop-filter:blur(14px); box-shadow:var(--shadow);
  overflow:hidden;
}
.band.k-bart{border-color:rgba(var(--accent-rgb),.5)}
.band.k-major{border-color:rgba(255,95,109,.55); background:rgba(38,16,22,.9)}
.band.k-closure{border-color:rgba(197,139,255,.45)}
.band-body{display:flex; align-items:center; gap:calc(var(--pad)*1.1); min-width:0; flex:1}
.band-kind{
  flex:none; font-size:var(--fs-xs); font-weight:800; letter-spacing:.09em; text-transform:uppercase;
  padding:.35em .8em; border-radius:.45em; background:var(--accent); color:#06101f; white-space:nowrap;
}
.band.k-major .band-kind{background:var(--alert); color:#fff}
.band.k-closure .band-kind{background:var(--purple); color:#1b0d2b}
.band-text{min-width:0}
.band-text{flex:1}
/* Long advisories used to be cut off with an ellipsis, which on an unattended
   screen means the sentence can never be read. The text now scrolls: .mq is
   the moving inner span, the b/span around it is the clipping window. */
.band-text b{display:block; font-size:var(--fs-lg); line-height:1.25;
  overflow:hidden; white-space:nowrap}
.band-text>span{display:block; font-size:var(--fs-sm); color:var(--dim); margin-top:.1em;
  overflow:hidden; white-space:nowrap}
.band-text .mq{display:inline-block; white-space:nowrap; will-change:transform}
.band-side{display:flex; align-items:center; gap:.7em; flex:none}
.band-pos{font-size:var(--fs-xs); color:var(--faint); font-variant-numeric:tabular-nums}
.band-dots{display:flex; gap:.3em}
.band-dots i{width:.42em; height:.42em; border-radius:50%; background:var(--faint); opacity:.4}
.band-dots i.on{opacity:1; background:var(--accent)}
.band.k-major .band-dots i.on{background:var(--alert)}
.band-progress{position:absolute; left:0; right:0; bottom:0; height:.18em; background:rgba(255,255,255,.07)}
.band-progress i{display:block; height:100%; width:0; background:var(--accent)}
.band.k-major .band-progress i{background:var(--alert)}
.band-body.swap{animation:swap .45s ease}
@keyframes swap{from{opacity:0; transform:translateY(.5em)} to{opacity:1; transform:none}}
@media (prefers-reduced-motion:reduce){.band-body.swap{animation:none}}

/* ---------- TV mode: ?tv=1 — no pointer, no controls, bigger everything ---- */
body.tv{--fs:clamp(15px, 0.86vw + 8px, 34px)}
body.tv .ghost,body.tv .layers,body.tv .tabs,body.tv .credits{display:none}
body.tv .panel{cursor:none}
body.tv.idle,body.tv.idle *{cursor:none}
body.tv .feed-list li,body.tv .roadgrid li{cursor:default}

/* ---------- map markers ---------- */
/* The train SVG is authored on a 34-unit canvas; let it scale to whatever box
   the engine gives it rather than overflowing at its intrinsic size. */
.train svg{display:block; width:100%; height:100%;
  filter:drop-shadow(0 2px 4px rgba(0,0,0,.65))}
/* Stopped trains were faded to .78, which undid the casing that makes them
   readable against their own line. Distinguish them by shape, not by hiding. */
.train.dwell svg{opacity:.94}
.stn{width:.6em; height:.6em; border-radius:50%; background:#0b1020;
  border:2px solid rgba(210,228,255,.85); box-shadow:0 0 0 1px rgba(0,0,0,.55)}
.inc{border-radius:50%; box-shadow:0 0 0 1px rgba(0,0,0,.5),0 2px 6px rgba(0,0,0,.5)}
.inc.major{background:var(--alert); width:14px; height:14px; border:2px solid #fff}
.inc.moderate{background:var(--warn); width:12px; height:12px; border:2px solid rgba(255,255,255,.85)}
.inc.minor{background:#8fa3c8; width:10px; height:10px; border:1.5px solid rgba(255,255,255,.7)}
.clo{width:11px; height:11px; background:var(--purple); border:1.5px solid #fff;
  transform:rotate(45deg); box-shadow:0 2px 5px rgba(0,0,0,.5)}

/* dark treatment for light raster tiles; scoped to tiles only */
.tiles-dark .leaflet-tile-pane{filter:invert(1) hue-rotate(180deg) saturate(.22) brightness(.92) contrast(.92)}

/* ---------- leaflet overrides ---------- */
.leaflet-container{background:var(--bg); font:inherit}
.leaflet-popup-content-wrapper{background:#101728; color:var(--ink); border:1px solid var(--edge);
  border-radius:12px; box-shadow:var(--shadow)}
.leaflet-popup-tip{background:#101728; border:1px solid var(--edge)}
.leaflet-popup-content{margin:.8em 1em; font-size:var(--fs-sm); line-height:1.5}
.leaflet-popup-close-button{color:var(--dim)!important}
.pop h3{margin:0 0 .35em; font-size:var(--fs-sm); display:flex; align-items:center; gap:.5em}
.pop .pill{font-size:var(--fs-xs); padding:.15em .55em; border-radius:99px; color:#08111f; font-weight:700}
.pop dl{margin:.4em 0 0; display:grid; grid-template-columns:auto 1fr; gap:.1em .8em}
.pop dt{color:var(--faint); font-size:var(--fs-xs)}
.pop dd{margin:0; font-size:var(--fs-xs); font-variant-numeric:tabular-nums}
.pop .note{margin:.4em 0 0; color:var(--dim); font-size:var(--fs-xs);
  border-top:1px solid var(--edge); padding-top:.35em}
.leaflet-control-attribution{background:rgba(8,11,20,.8)!important; color:var(--faint)!important;
  font-size:var(--fs-xs)!important}
.leaflet-control-attribution a{color:var(--dim)!important}
.leaflet-bar a{background:#101728!important; color:var(--ink)!important; border-color:var(--edge)!important}
body.tv .leaflet-control-zoom{display:none}

/* ---------- mobile ---------- */
/* The desktop layout is a fixed map with floating panels. On a phone that
   cannot work: an absolutely-positioned panel does not extend the document's
   scroll height, so everything below the fold becomes unreachable. Mobile
   therefore puts the map and panel into normal flow and lets the page scroll. */
@media (max-width:820px){
  html,body{height:auto; overflow:visible}
  body{overflow-x:hidden}
  #mapwrap{
    position:relative; inset:auto; width:100%; height:46vh;
    border-bottom:1px solid var(--edge);
  }
  .bar{gap:.5em; padding:.5em .6em}
  .brand h1{font-size:var(--fs)}
  /* The header overlays the map's top-left corner, where Leaflet puts its zoom
     buttons - they clipped the wordmark and the first stat. Pinch-zoom covers
     this on touch, so drop the control rather than fight it. */
  .leaflet-control-zoom{display:none}
  .leaflet-control-attribution{font-size:9px!important}
  .meta{order:2} .meta #updated{display:none} .meta .ghost{display:none}
  /* ...except the presentation toggle, which is the one control a phone still
     needs. The panel toggle stays hidden -- mobile always shows the panel. */
  .meta #btn-present{display:inline-block; font-size:var(--fs-xs); padding:.3em .6em}
  .stats{width:100%; order:3; margin-left:0; gap:.3em}
  .stat{flex:1; min-width:0; padding:.25em .3em}
  .stat span{font-size:9px; letter-spacing:.03em}
  .panel{
    position:static; width:auto; max-height:none; overflow:visible;
    margin:0; border-radius:var(--r) var(--r) 0 0; border-bottom:0;
    /* The alert band is fixed over the bottom, so leave room to scroll clear. */
    padding-bottom:calc(var(--band-h,80px) + var(--pad)*2 + env(safe-area-inset-bottom));
  }
  .panel.hide{display:flex}            /* the toggle is desktop-only */
  .feed-list{max-height:none; overflow:visible}
  .roadgrid{grid-template-columns:repeat(auto-fill,minmax(5.6em,1fr))}
  .band{
    left:0; right:0; bottom:0; border-radius:0; border-left:0; border-right:0; z-index:700;
    padding-bottom:calc(var(--pad)*.9 + env(safe-area-inset-bottom));
  }
  .band-side{display:none}
}
@media (max-width:820px) and (orientation:landscape){
  #mapwrap{height:62vh}
}

/* ---------- presentation mode ---------- */
/* One BART line at a time, then live traffic. The side panel is hidden so the
   room sees a single uncluttered subject per stage. */
body.present .panel{display:none}
.stage{
  /* Anchored right, deliberately. The Bay Area's water is all to the west, so a
     card on the left forces the map fit to reserve the western side of the
     screen -- and the spare canvas beyond the fit then fills with ocean. On the
     right, the spare canvas is inland instead. */
  position:fixed; z-index:650; right:var(--pad); top:calc(var(--pad)*2 + var(--fs)*3.6);
  width:clamp(300px, 31vw, 640px);
  display:flex; flex-direction:column; gap:calc(var(--pad)*.8);
  padding:calc(var(--pad)*1.1) calc(var(--pad)*1.3) calc(var(--pad)*1.5);
  border:1px solid var(--edge); border-left:6px solid var(--line,var(--accent));
  border-radius:var(--r); background:var(--panel); backdrop-filter:blur(14px);
  box-shadow:var(--shadow); overflow:hidden;
}
.stage-head{display:flex; align-items:center; gap:.7em}
.stage-swatch{
  width:1.15em; height:1.15em; border-radius:50%; flex:none;
  background:var(--line,var(--accent)); box-shadow:0 0 0 3px rgba(255,255,255,.09);
}
.stage-head h2{font-size:var(--fs-xl); line-height:1.08}
.stage-head p{margin:.15em 0 0; color:var(--dim); font-size:var(--fs-sm);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:24em}
.stage-pos{margin-left:auto; align-self:flex-start; color:var(--faint);
  font-size:var(--fs-xs); font-variant-numeric:tabular-nums; flex:none}
.stage-stats{display:grid; grid-template-columns:repeat(4,1fr); gap:.5em}
.stage-stats div{
  border:1px solid var(--edge); border-radius:.7em; padding:.4em .35em;
  text-align:center; background:rgba(255,255,255,.03);
}
.stage-stats b{display:block; font-size:var(--fs-lg); font-variant-numeric:tabular-nums; line-height:1.1}
.stage-stats .warn{color:var(--warn)} .stage-stats .alert{color:var(--alert)}
.stage-stats span{font-size:var(--fs-xs); color:var(--dim); text-transform:uppercase; letter-spacing:.06em}
.stage-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:.3em}
.stage-list li{
  display:flex; align-items:baseline; gap:.6em; font-size:var(--fs-sm);
  border-top:1px solid var(--edge); padding-top:.32em;
}
.stage-list li .n{color:var(--accent); font-weight:700; font-variant-numeric:tabular-nums;
  flex:none; min-width:1.8em; text-align:right}
.stage-list li .d{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.stage-list li .m{color:var(--faint); font-size:var(--fs-xs); flex:none}
.stage-list li.empty{color:var(--faint); border-top:0}
.stage-progress{position:absolute; left:0; right:0; bottom:0; height:.18em; background:rgba(255,255,255,.07)}
.stage-progress i{display:block; height:100%; width:0; background:var(--line,var(--accent))}
/* The button is the only control that survives TV mode, so the room can start
   and stop a cycle without a keyboard. It fades with the cursor. */
body.tv #btn-present{display:inline-block}
body.tv.idle #btn-present{opacity:0; transition:opacity .4s}
#btn-present[aria-pressed="true"]{border-color:var(--accent); color:var(--accent)}

@media (max-width:820px){
  .stage{
    position:static; width:auto; right:auto; margin:var(--pad) var(--pad) 0;
    margin-bottom:calc(var(--band-h,80px) + var(--pad));
  }
  .stage-head h2{font-size:var(--fs-lg)}
  .stage-stats{gap:.35em}
  body.present .bar .stats{display:none}
}

/* .band and .stage both set display, which outranks the UA [hidden] rule --
   without this they render empty before their first data arrives. */
[hidden]{display:none!important}

/* Reduced-motion readers get the full text wrapped rather than scrolled. */
.band-text.wrap b,.band-text.wrap>span{white-space:normal; overflow:visible}
.band-text.wrap .mq{display:inline}
.band-text.wrap b{display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden}

.stage.swap{animation:swap .5s ease}
@media (prefers-reduced-motion:reduce){.stage.swap{animation:none}}

/* Severity badge on the traffic stage list. Colour carries the meaning at a
   distance; the three-letter tag disambiguates major from moderate up close. */
.stage-list li .sv{
  min-width:3.1em; font-size:var(--fs-xs); font-weight:800; letter-spacing:.06em;
}
.stage-list li .sv.major{color:var(--alert)}
.stage-list li .sv.moderate{color:var(--warn)}
.stage-list li .sv.minor{color:var(--faint)}

/* ---------- rendering quality ---------- */
/* Markers are rotated and repositioned every frame, and Leaflet's vector layers
   are redrawn on every pan, both of which default to speed over smoothness in
   some browsers. Ask for geometric precision explicitly so edges stay clean. */
svg{shape-rendering:geometricPrecision; text-rendering:optimizeLegibility}
.leaflet-overlay-pane svg,.train svg{shape-rendering:geometricPrecision}
.train svg{backface-visibility:hidden}
/* Sub-pixel jitter shows up as shimmer on a large screen; compositing these on
   their own layer keeps the edges stable while they move. */
.band-text .mq,.stage.swap,.band-body.swap{backface-visibility:hidden; transform-style:preserve-3d}

/* ---------- presentation transitions ---------- */
/* A stage change moves the camera for FLY ms, so the card should settle over a
   comparable time rather than snapping ahead of the map. */
@keyframes stagein{
  from{opacity:0; transform:translate3d(-1.1em,0,0) scale(.985)}
  to{opacity:1; transform:none}
}
.stage.swap{animation:stagein .62s cubic-bezier(.22,.61,.36,1) both; will-change:transform,opacity}
.stage-list li{animation:stagein .5s cubic-bezier(.22,.61,.36,1) both}
.stage-list li:nth-child(2){animation-delay:.05s}
.stage-list li:nth-child(3){animation-delay:.1s}
.stage-list li:nth-child(4){animation-delay:.15s}
.stage-list li:nth-child(5){animation-delay:.2s}
.stage-list li:nth-child(6){animation-delay:.25s}
.stage-progress i{transition:width .18s linear}
@media (prefers-reduced-motion:reduce){
  .stage.swap,.stage-list li{animation:none}
  .stage-progress i{transition:none}
}

/* ---------- motion + polish ---------- */
/* A number that changes while nobody is looking should say so. This is the
   difference between a board that is live and a board that might be frozen. */
@keyframes bump{
  0%{transform:scale(1)}
  28%{transform:scale(1.16); filter:brightness(1.6)}
  100%{transform:scale(1); filter:none}
}
.stat b.bump{animation:bump .52s var(--ease)}

/* Rows only animate when the list actually changed -- see renderFeed. */
@keyframes rowin{from{opacity:0; transform:translateY(.4em)} to{opacity:1; transform:none}}
.feed-list li.fresh,.roadgrid li.fresh{
  animation:rowin .4s var(--ease) both;
  animation-delay:calc(var(--i,0) * 26ms);
}

/* Everything that answers a pointer should look like it does. */
.ghost,.tab,.roadgrid li,.feed-list li,.sw,.sw::after{
  transition:background .18s var(--ease), border-color .18s var(--ease),
             color .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease);
}
.ghost:hover{transform:translateY(-1px); box-shadow:0 4px 14px rgba(var(--accent-rgb),.24)}
.ghost:active{transform:translateY(0)}
.roadgrid li:hover,.feed-list li:hover{transform:translateY(-1px)}
.tab:hover{color:var(--ink); border-color:rgba(var(--accent-rgb),.45)}

/* The wordmark carries the livery, so give it some depth. */
.brand h1 span{text-shadow:0 0 18px rgba(var(--accent-rgb),.45)}

/* Panels float over a map; a hairline top highlight stops them reading flat. */
.stat,.panel,.stage,.band{box-shadow:var(--shadow), inset 0 1px 0 rgba(255,255,255,.055)}
.stat{transition:border-color .2s var(--ease)}
.stat.alert{border-color:rgba(255,95,109,.34)}
.stat.warn{border-color:rgba(255,182,72,.32)}
.stage-swatch{box-shadow:0 0 0 3px rgba(255,255,255,.09), 0 0 16px rgba(var(--accent-rgb),.35)}

@media (prefers-reduced-motion:reduce){
  .stat b.bump,.feed-list li.fresh,.roadgrid li.fresh{animation:none}
  .ghost:hover,.roadgrid li:hover,.feed-list li:hover{transform:none}
}

/* ---------- last-updated ticker ---------- */
/* Sits beside the clock. Green-ish while fresh, amber past a minute, red past
   three -- the same thresholds the status dot uses, so they never disagree. */
.ticker{
  display:inline-flex; align-items:baseline; gap:.42em;
  padding:.3em .7em; border:1px solid var(--edge); border-radius:.55em;
  background:var(--panel); white-space:nowrap;
}
.ticker-k{
  font-size:var(--fs-xs); color:var(--dim);
  text-transform:uppercase; letter-spacing:.08em;
}
#ticker-ago{
  font-size:var(--fs-sm); font-variant-numeric:tabular-nums; font-weight:650;
  color:var(--accent); min-width:5.4ch; text-align:right;
}
#ticker-ago.bump{animation:bump .52s var(--ease)}
.ticker.warn{border-color:rgba(255,182,72,.34)}
.ticker.warn #ticker-ago{color:var(--warn)}
.ticker.stale{border-color:rgba(255,95,109,.4); background:rgba(38,16,22,.86)}
.ticker.stale #ticker-ago{color:var(--alert)}
@media (prefers-reduced-motion:reduce){#ticker-ago.bump{animation:none}}

@media (max-width:820px){
  /* Swap it in for the upstream-feed age rather than adding to the row: the
     ticker is the more useful of the two on a phone, and this costs no width. */
  .meta #ticker{display:inline-flex; padding:.22em .5em}
  .meta .ticker-k{font-size:9px; letter-spacing:.04em}
  .meta #ticker-ago{font-size:var(--fs-xs); min-width:4.6ch}
}

/* ---------- zoom control placement ---------- */
/* The alert band spans the full width of the bottom, so a bottom-right zoom
   control sits underneath it. Both engines put it at the right-hand middle
   instead, clear of the band and of the stats in the top bar. */
.leaflet-top.leaflet-right{top:50%; transform:translateY(-50%)}
.leaflet-top.leaflet-right .leaflet-control{margin-top:0}
