/* ProbeHawk dashboard - dark, dense, readable at a glance from across a room. */

:root {
  --bg: #0b0f14;
  --bg-panel: #121820;
  --bg-raised: #182029;
  --line: #232e3a;
  --text: #dfe7ef;
  --text-dim: #8b9aab;
  --text-faint: #5f6d7d;

  --healthy: #35d07f;
  --minor: #b9d13a;
  --degraded: #f0b429;
  --impaired: #f5803e;
  --critical: #ef4d5a;
  --accent: #58a6ff;

  --radius: 10px;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
}

* { box-sizing: border-box; }

/* Any author rule that sets `display` silently defeats the browser's
   `[hidden] { display: none }`, because author styles beat the UA stylesheet
   regardless of specificity. That is how the trace modal ended up rendering
   over the whole dashboard from first paint despite carrying `hidden`.
   Nothing marked hidden should ever be laid out, so enforce it once here. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ---------------------------------------------------------------- topbar */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 12px 20px;
  background: linear-gradient(180deg, #0e141b, #0b0f14);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 60;   /* above the watermark overlay */
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand h1 { font-size: 16px; margin: 0; letter-spacing: .3px; font-weight: 650; }
/* ------------------------------------------------------- region picker */
.region-picker { position: relative; }
.region {
  color: var(--text); font-size: 13px; padding: 4px 10px 4px 12px;
  border: 1px solid transparent; border-left: 1px solid var(--line);
  border-radius: 6px; background: transparent; cursor: pointer;
  display: flex; align-items: center; gap: 7px; font-family: inherit;
}
.region:hover { border-color: var(--line); border-left-color: var(--accent); background: var(--bg-raised); }
.region .caret { color: var(--text-faint); font-size: 10px; }
.region-menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;
  background: var(--bg-panel); border: 1px solid var(--line);
  border-radius: 8px; padding: 5px; min-width: 230px;
  box-shadow: 0 12px 32px rgba(0,0,0,.55);
  max-height: 60vh; overflow-y: auto;
}
.region-opt {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  width: 100%; text-align: left; background: transparent; border: 0;
  color: var(--text); padding: 7px 10px; border-radius: 5px;
  font-size: 13px; cursor: pointer; font-family: inherit;
}
.region-opt:hover { background: var(--bg-raised); }
/* Sticky so it stays reachable while scrolling eighty-odd regions. */
.region-search {
  position: sticky; top: 0; z-index: 1;
  width: 100%; box-sizing: border-box; margin-bottom: 6px;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-radius: 6px; padding: 6px 9px;
  color: var(--text); font: inherit; font-size: 13px;
}
.region-search::placeholder { color: var(--text-faint); }
.region-search:focus { outline: none; border-color: var(--accent); }
.region-group:not(:first-of-type) { margin-top: 8px; }

.region-opt.active { color: var(--accent); }
.region-opt .code { font-family: var(--mono); font-size: 11px; color: var(--text-faint); }
.region-opt.active .code { color: var(--accent); }
/* The dot carries the health band, set by renderGauge so it can never drift
   from the gauge. Everything hangs off `color` and currentColor, so JS assigns
   one property and the fill plus the pulse ring both follow. */
.dot {
  width: 9px; height: 9px; border-radius: 50%;
  color: var(--dot-band, var(--healthy));
  background: currentColor;
  box-shadow: 0 0 0 0 currentColor;
  animation: pulse 2.4s infinite;
}
/* Stale is grey, not red: it means "no fresh data", which is not the same
   claim as "the network is critical" - and red would be indistinguishable
   from a genuine critical band. Dropping the pulse says it too. */
.dot.stale { color: var(--text-faint); animation: none; }
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 50%, transparent); }
  70%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.updated { color: var(--text-faint); font-size: 12px; font-family: var(--mono); }

.btn {
  background: var(--bg-raised); color: var(--text); border: 1px solid var(--line);
  border-radius: 6px; padding: 6px 12px; font-size: 12px; cursor: pointer;
  transition: border-color .15s, background .15s;
}
.btn:hover { border-color: var(--accent); background: #1d2733; }
.btn:disabled { opacity: .5; cursor: wait; }
.btn-ghost { background: transparent; }

/* ------------------------------------------------------------------ main */
main { padding: 18px 20px 40px; max-width: 1680px; margin: 0 auto; }

.panel {
  background: var(--bg-panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px; margin-bottom: 16px;
}
.panel h2 {
  font-size: 12px; text-transform: uppercase; letter-spacing: .8px;
  color: var(--text-dim); margin: 0 0 12px; font-weight: 600;
  display: flex; align-items: baseline; gap: 10px;
}
.hint { text-transform: none; letter-spacing: 0; color: var(--text-faint); font-weight: 400; }
.panel.wide { width: 100%; }

/* Every long list gets a ceiling and its own scrollbar - the pattern the
   incident feed already used, which is why the middle column stayed sane
   while the left ran to 3000px. A panel whose height depends on how many
   backbones exist is a panel that cannot be laid out against its neighbours.
   Nothing is hidden: the list scrolls, it just stops dictating the page. */
.probe-list { max-height: 290px; }
.mini-list  { max-height: 320px; }
.asn-list   { max-height: 240px; }
/* scrollbar-gutter reserves the track's width up front, so a row laid out as
   `1fr 60px` still fits once the bar appears - without it the list overflowed
   horizontally by exactly the bar's width. overflow-x is the backstop for
   browsers that do not support the gutter yet. */
.probe-list, .mini-list, .asn-list {
  overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable;
}
/* Grid rows inside a scrolling flex column keep their pre-scrollbar width
   unless told otherwise - `min-width: auto` is the default on a grid item and
   refuses to shrink below its tracks. Without this the rows sat 6px wider than
   the list and the right edge of each row was quietly clipped. */
.probe, .asn { min-width: 0; max-width: 100%; }

/* Thin, dark scrollbars so a bounded list does not announce itself. */
.probe-list::-webkit-scrollbar, .mini-list::-webkit-scrollbar,
.asn-list::-webkit-scrollbar, .feed::-webkit-scrollbar { width: 8px; }
.probe-list::-webkit-scrollbar-thumb, .mini-list::-webkit-scrollbar-thumb,
.asn-list::-webkit-scrollbar-thumb, .feed::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: 4px;
}

/* Merged panels group what used to be separate boxes. A sub-block gets air
   and a hairline rather than its own border - the panel border already says
   "these belong together", and boxes inside boxes is what made the page busy. */
.sub-block + .sub-block {
  margin-top: 14px; padding-top: 13px; border-top: 1px solid var(--line);
}

/* Columns end level with each other. The grid row is as tall as the tallest
   column, and each column's *last* panel absorbs whatever slack is left, so
   the three bottoms line up instead of stopping wherever their content ran
   out. Slack lands on the last scrollable list inside that panel, which grows
   to fill and keeps scrolling if the content outruns the space. */
.columns { display: grid; grid-template-columns: 1fr 1.25fr 1fr; gap: 16px; align-items: stretch; }
.col { display: flex; flex-direction: column; min-width: 0; }
.col > .panel:last-child {
  flex: 1 1 auto; margin-bottom: 0;
  display: flex; flex-direction: column; min-height: 0;
}
/* The block that takes the slack: either the panel's last sub-block, or the
   incident feed when the panel has no sub-blocks. */
.col > .panel:last-child > .sub-block:last-child,
.col > .panel:last-child > .feed {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
}
/* max-height gave every list a ceiling so no panel could dictate the page.
   The one stretching list is exempt: its height is now the column's, which is
   already bounded by the tallest column. */
.col > .panel:last-child > .sub-block:last-child > .mini-list,
.col > .panel:last-child > .sub-block:last-child > .asn-list,
.col > .panel:last-child > .sub-block:last-child > .probe-list,
.col > .panel:last-child > .feed {
  flex: 1 1 auto; max-height: none; min-height: 0;
}

/* Stacked on one column there is no neighbour to line up with, and a panel
   stretched to fill a row of its own is just a box of empty space. */
@media (max-width: 900px) {
  .col > .panel:last-child { flex: 0 0 auto; margin-bottom: 16px; display: block; }
  .col > .panel:last-child > .sub-block:last-child { display: block; }
  .col > .panel:last-child > .sub-block:last-child > .mini-list { max-height: 320px; }
  .col > .panel:last-child > .sub-block:last-child > .asn-list  { max-height: 240px; }
  .col > .panel:last-child > .sub-block:last-child > .probe-list { max-height: 290px; }
  .col > .panel:last-child > .feed { max-height: 640px; flex: 0 0 auto; }
}
@media (max-width: 1280px) { .columns { grid-template-columns: 1fr 1fr; } }
@media (max-width: 900px)  { .columns { grid-template-columns: 1fr; } }

/* ------------------------------------------------------------------ hero */
/* Metrics and map occupy one slot and swap, so the top of the page stays a
   single screen-height summary instead of two stacked ones. */
.hero-shell {
  background: var(--bg-panel); border: 1px solid var(--line);
  border-radius: var(--radius); margin-bottom: 16px; overflow: hidden;
}
.view-tabs {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-bottom: 1px solid var(--line);
}
.view-tabs .hint { margin-left: 4px; }
.view-tabs .map-zoom { margin-left: auto; display: flex; gap: 6px; }

.hero-map { padding: 14px 16px 12px; }

.hero {
  display: grid; grid-template-columns: 260px 1fr; gap: 20px;
  padding: 18px;
}
@media (max-width: 760px) { .hero { grid-template-columns: 1fr; } }

.gauge-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.gauge { width: 200px; height: 200px; transform: rotate(-90deg); }
.gauge-track, .gauge-value {
  fill: none; stroke-width: 14; stroke-linecap: round;
}
.gauge-track { stroke: #1c2530; }
.gauge-value {
  stroke: var(--healthy);
  stroke-dasharray: 515;
  stroke-dashoffset: 515;
  transition: stroke-dashoffset .8s cubic-bezier(.2,.7,.2,1), stroke .4s;
}
.gauge-num, .gauge-sub {
  transform: rotate(90deg); transform-origin: 100px 100px;
  text-anchor: middle; fill: var(--text);
}
.gauge-num { font-size: 46px; font-weight: 300; font-family: var(--mono); }
.gauge-sub { font-size: 11px; fill: var(--text-faint); letter-spacing: 1px; text-transform: uppercase; }
.gauge-band {
  font-size: 13px; font-weight: 600; letter-spacing: .4px;
  padding: 4px 14px; border-radius: 20px; border: 1px solid currentColor;
}

.hero-side { display: flex; flex-direction: column; gap: 14px; justify-content: center; }
.cat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.cat {
  background: var(--bg-raised); border: 1px solid var(--line);
  border-radius: 8px; padding: 10px 12px;
}
.cat-name { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .6px; }
.cat-score { font-size: 22px; font-family: var(--mono); font-weight: 400; margin-top: 2px; }
.cat-meta { font-size: 11px; color: var(--text-faint); }

.spark-wrap { background: var(--bg-raised); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
.spark-head {
  display: flex; justify-content: space-between; font-size: 11px;
  color: var(--text-faint); margin-bottom: 6px; text-transform: uppercase; letter-spacing: .6px;
}
.spark { width: 100%; height: 90px; display: block; }

/* --------------------------------------------------------------- drivers */
.drivers { display: flex; flex-wrap: wrap; gap: 8px; }
.driver {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-left: 3px solid var(--degraded);
  border-radius: 6px; padding: 8px 12px; font-size: 13px;
  text-decoration: none; color: var(--text);
  /* min() so the cap can never exceed the container - a flat 460px was wider
     than a phone viewport and pushed the whole page into horizontal scroll. */
  max-width: min(460px, 100%);
}
.driver:hover { border-color: var(--accent); }
.driver .pts { font-family: var(--mono); font-size: 12px; color: var(--critical); white-space: nowrap; }
/* min-width: 0 is what makes the ellipsis work at all: a flex child defaults
   to min-width auto and refuses to shrink below its text, so it overflowed
   instead of truncating. */
.driver .txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.empty { color: var(--text-faint); font-size: 13px; padding: 8px 0; }

/* ---------------------------------------------------------------- probes */
.probe-list { display: flex; flex-direction: column; gap: 6px; }
.probe {
  display: grid; grid-template-columns: 1fr 60px; gap: 8px; align-items: center;
  font-size: 13px; padding: 4px 0;
}
.probe-name { display: flex; align-items: center; gap: 8px; overflow: hidden; }
.probe-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pill { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.pill.ok { background: var(--healthy); }
.pill.degraded { background: var(--degraded); }
.pill.down { background: var(--critical); }
.probe-bar { height: 3px; background: #1c2530; border-radius: 2px; margin-top: 4px; overflow: hidden; }
.probe-bar > i { display: block; height: 100%; background: var(--accent); }
.probe-ms { font-family: var(--mono); font-size: 12px; text-align: right; color: var(--text-dim); }
.probe-ms.bad { color: var(--critical); }
.tier { font-size: 10px; color: var(--text-faint); border: 1px solid var(--line);
        border-radius: 3px; padding: 0 4px; text-transform: uppercase; }

/* ------------------------------------------------------------------ asns */
.asn-list { display: flex; flex-direction: column; gap: 4px; }
.asn {
  display: grid; grid-template-columns: 1fr auto auto; gap: 10px;
  align-items: center; font-size: 13px; padding: 5px 0;
  border-bottom: 1px solid rgba(35,46,58,.5);
}
.asn:last-child { border-bottom: 0; }
.asn-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.asn-pfx { font-family: var(--mono); font-size: 12px; color: var(--text-dim); }
.asn-delta { font-family: var(--mono); font-size: 11px; padding: 1px 6px; border-radius: 4px; }
.asn-delta.flat { color: var(--text-faint); }
.asn-delta.down { color: var(--critical); background: rgba(239,77,90,.12); }
.asn-delta.up { color: var(--healthy); }

/* ------------------------------------------------------------------ feed */
.filters { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
.chip {
  background: transparent; border: 1px solid var(--line); color: var(--text-dim);
  border-radius: 20px; padding: 3px 12px; font-size: 12px; cursor: pointer;
}
.chip-on { background: var(--bg-raised); color: var(--text); border-color: var(--accent); }

.feed { display: flex; flex-direction: column; gap: 8px; max-height: 640px; overflow-y: auto; }
.item {
  border: 1px solid var(--line); border-left: 3px solid var(--minor);
  border-radius: 6px; padding: 10px 12px; background: var(--bg-raised);
}
.item-head { display: flex; align-items: baseline; gap: 8px; justify-content: space-between; }
.item-title { font-size: 13px; font-weight: 550; line-height: 1.35; }
.item-title a { color: inherit; text-decoration: none; }
.item-title a:hover { color: var(--accent); text-decoration: underline; }
.item-detail { font-size: 12px; color: var(--text-dim); margin-top: 5px; line-height: 1.45; }
.item-meta { display: flex; gap: 6px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.tag {
  font-size: 10px; text-transform: uppercase; letter-spacing: .5px;
  border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; color: var(--text-faint);
}
.tag.sev { border-color: currentColor; }
.tag.here { color: var(--accent); border-color: var(--accent); }
.ago { font-size: 11px; color: var(--text-faint); font-family: var(--mono); white-space: nowrap; }

/* -------------------------------------------------------------- services */
.svc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 6px; }
.svc {
  border: 1px solid var(--line); border-radius: 6px; padding: 7px 8px;
  background: var(--bg-raised); text-decoration: none; color: var(--text);
  display: flex; flex-direction: column; gap: 3px; min-height: 48px;
}
.svc:hover { border-color: var(--accent); }
.svc-name { font-size: 12px; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.svc-state { font-size: 10px; text-transform: uppercase; letter-spacing: .4px; }
.svc.none    { border-left: 3px solid var(--healthy); }
.svc.minor   { border-left: 3px solid var(--minor); }
.svc.major   { border-left: 3px solid var(--impaired); }
.svc.critical{ border-left: 3px solid var(--critical); }
.svc.maintenance { border-left: 3px solid var(--accent); }
.svc.unknown { border-left: 3px solid var(--text-faint); opacity: .6; }

/* ------------------------------------------------------------ mini lists */
.mini-list { display: flex; flex-direction: column; gap: 7px; max-height: 340px; overflow-y: auto; }
.mini {
  font-size: 12px; line-height: 1.4; padding-bottom: 7px;
  border-bottom: 1px solid rgba(35,46,58,.5);
}
.mini:last-child { border-bottom: 0; }
.mini a { color: var(--text); text-decoration: none; }
.mini a:hover { color: var(--accent); }
.mini .sub { color: var(--text-faint); font-size: 11px; margin-top: 2px; font-family: var(--mono); }
.mini.tier-high { border-left: 2px solid var(--critical); padding-left: 8px; }
.mini.tier-medium { border-left: 2px solid var(--impaired); padding-left: 8px; }

/* escalation badge in the news panel header */
.esc { padding: 1px 8px; border-radius: 10px; border: 1px solid currentColor; font-size: 10px;
       text-transform: uppercase; letter-spacing: .5px; }
.esc.quiet { color: var(--healthy); }
.esc.watch { color: var(--minor); }
.esc.elevated { color: var(--impaired); }
.esc.severe { color: var(--critical); }

footer {
  color: var(--text-faint); font-size: 11px; text-align: center;
  padding: 16px 20px 28px; border-top: 1px solid var(--line);
}

/* scrollbars that do not look like 2003 */
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #26313d; border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: #33404f; }

/* ------------------------------------------------- core infrastructure */
.core-sub {
  font-size: 11px; color: var(--text-dim); text-transform: uppercase;
  letter-spacing: .6px; margin: 12px 0 7px; display: flex; gap: 8px; align-items: baseline;
}
.core-sub:first-of-type { margin-top: 0; }
.root-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(66px, 1fr)); gap: 5px; }
.root {
  border: 1px solid var(--line); border-top: 2px solid var(--healthy);
  border-radius: 5px; padding: 5px 6px; background: var(--bg-raised);
  display: flex; flex-direction: column; gap: 1px; cursor: default;
}
.root.down { border-top-color: var(--critical); background: rgba(239,77,90,.08); }
.root.slow { border-top-color: var(--degraded); }
.root-letter { font-family: var(--mono); font-size: 12px; font-weight: 600; text-transform: uppercase; }
.root-ms { font-family: var(--mono); font-size: 10px; color: var(--text-faint); }

.panel-note { font-size: 11px; color: var(--text-faint); margin: -4px 0 10px; }
.probe { cursor: pointer; border-radius: 5px; padding: 4px 6px; margin: 0 -6px; }
.probe:hover { background: var(--bg-raised); }

/* ------------------------------------------------------------- modal */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(5,8,12,.78); z-index: 100;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
/* Kept alongside the global [hidden] rule at the top of this file: this is the
   selector that actually regressed, and it should stay obvious at the site. */
.modal-backdrop[hidden] { display: none; }
.modal {
  background: var(--bg-panel); border: 1px solid var(--line); border-radius: 10px;
  width: min(760px, 100%); max-height: 82vh; display: flex; flex-direction: column;
  box-shadow: 0 24px 64px rgba(0,0,0,.6);
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
.modal-head h3 { margin: 0; font-size: 14px; font-weight: 600; }
.modal-body { padding: 14px 16px; overflow-y: auto; }

.hop-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.hop-table th {
  text-align: left; font-weight: 500; color: var(--text-faint); font-size: 10px;
  text-transform: uppercase; letter-spacing: .6px; padding: 0 8px 6px 0;
  border-bottom: 1px solid var(--line);
}
.hop-table td { padding: 6px 8px 6px 0; border-bottom: 1px solid rgba(35,46,58,.45); }
.hop-table td.num, .hop-table td.ip, .hop-table td.ms { font-family: var(--mono); }
.hop-table tr.dead td { color: var(--text-faint); }
.hop-table tr.dead td.ip::before { content: '· · ·'; }
.hop-table tr.breakpoint td { background: rgba(239,77,90,.10); }
.hop-table td.ms.high { color: var(--degraded); }
.trace-verdict {
  font-size: 13px; padding: 10px 12px; border-radius: 6px; margin-bottom: 12px;
  background: var(--bg-raised); border-left: 3px solid var(--accent);
}
.trace-verdict.bad { border-left-color: var(--critical); }

/* Rows with nothing traceable behind them should not look clickable. */
.probe.no-trace { cursor: default; }
.probe.no-trace:hover { background: transparent; }

/* Category cards: say what is measured and which direction is good. */
.scale-note {
  font-size: 11px; color: var(--text-faint); margin: 0 0 2px; line-height: 1.5;
}
.scale-note strong { color: var(--text-dim); font-weight: 600; }
.cat { cursor: help; }
.cat-outof { font-size: 12px; color: var(--text-faint); margin-left: 1px; }
.cat-meta { line-height: 1.35; }

/* Plain-English "what is this vendor" note on service incidents. */
.item-why {
  font-size: 11px; color: var(--text-dim); margin-top: 6px;
  padding-left: 8px; border-left: 2px solid var(--line);
  display: flex; flex-direction: column; gap: 3px;
}
.item-why .scoped { color: var(--text-faint); font-style: italic; }

/* Panel note that changes meaning when viewing a region you are not in. */
.panel-note.warn {
  color: var(--degraded); border-left: 2px solid var(--degraded);
  padding-left: 8px; line-height: 1.45;
}

/* Browser-vantage panel: visually distinct from the server-vantage one, since
   the whole point is that they measure different things. */
#panel-browser { border-left: 3px solid var(--accent); }
#panel-browser h2 { color: var(--accent); }

/* ------------------------------------------------------------ world map */
:root { --map-land: #1b2531; --map-sea: #0d131b; }

.worldmap {
  background: radial-gradient(ellipse at 50% 45%, #101822 0%, var(--map-sea) 70%);
  border: 1px solid var(--line); border-radius: 8px;
  overflow: hidden; position: relative;
  /* Fixed, modest height. The map is a component of the dashboard, not the
     first screen of it - and `slice` keeps it filled at any aspect ratio. */
  height: 300px;
  /* Cap the width so the frame is map-shaped rather than a letterbox slot. On
     an ultrawide monitor an unconstrained panel gave a 5.8:1 window - 1100 km
     across but only 210 km tall, which cropped out every neighbouring city. */
  max-width: 1040px; margin: 0 auto;
}
@media (max-width: 900px) { .worldmap { height: 230px; } }
.worldmap-svg { width: 100%; height: 100%; display: block; }

.map-zoom { margin-left: auto; margin-bottom: 0; }
.map-message {
  display: flex; align-items: center; justify-content: center;
  height: 100%; padding: 20px; text-align: center;
  font-size: 13px; color: var(--text-faint);
}
.map-message.error { color: var(--degraded); }

.country { stroke: #0b0f14; transition: fill .6s ease; }
.country.hit { stroke: rgba(239,77,90,.35); }
/* Bright enough to actually read as borders when zoomed in - this layer
   is the only geography the local view has. */
.admin1 { fill: none; stroke: rgba(139,154,171,.55); }

/* Distance rings: these are the radii that actually govern local scoring. */
.ring { fill: none; stroke: rgba(88,166,255,.28); stroke-dasharray: 4 4; }
.ring-label {
  fill: var(--text-faint); text-anchor: middle;
  paint-order: stroke; stroke: #0b0f14; stroke-linejoin: round;
}

.region-mark .dot {
  fill: var(--accent); stroke: #0b0f14; cursor: pointer; transition: fill .3s ease;
}
.region-mark .dot:hover { fill: #fff; }
.region-mark .dot:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.region-mark .dot.in-outage { fill: var(--critical); }
.region-mark.active .dot { fill: var(--healthy); }

.region-mark .ping {
  fill: none; stroke: var(--healthy);
  animation: mapping 2.2s ease-out infinite; pointer-events: none;
}
@keyframes mapping {
  0%   { r: var(--ping-from, 4px); opacity: .85; }
  100% { r: var(--ping-to, 26px);  opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .region-mark .ping { animation: none; opacity: .3; }
}

.map-label {
  fill: var(--text); font-weight: 600;
  paint-order: stroke; stroke: #0b0f14; stroke-linejoin: round;
}

.outage-point { fill: rgba(240,180,41,.75); stroke: var(--degraded); }
.outage-point.planned { fill: rgba(95,109,125,.5); stroke: var(--text-faint); }
.incident-point { fill: var(--text-faint); opacity: .8; }
.incident-point.damage { fill: var(--impaired); opacity: 1; }

.map-legend {
  display: flex; flex-wrap: wrap; gap: 14px; margin-top: 9px;
  font-size: 11px; color: var(--text-faint);
}
.map-legend span { display: flex; align-items: center; gap: 6px; }
.swatch { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.swatch.dot { background: var(--accent); }
.swatch.active { background: var(--healthy); }
.swatch.outage { background: rgba(240,180,41,.75); border: 1px solid var(--degraded); }
.swatch.incident { background: var(--impaired); border-radius: 2px; transform: rotate(45deg); }
.swatch.bad { background: rgb(214 62 74); border-radius: 2px; }

/* City reference points: the local view's sense of place. */
.place-dot { fill: rgba(223,231,239,.45); }
.place-label {
  fill: rgba(223,231,239,.62); font-weight: 500;
  paint-order: stroke; stroke: #0b0f14; stroke-linejoin: round;
  pointer-events: none;
}

/* --------------------------------------------------------------- branding */
.brand h1 {
  background: linear-gradient(92deg, var(--text) 0%, var(--accent) 130%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: var(--text);
}

/* Page-wide watermark. Fixed rather than absolute so it stays put while the
   dashboard scrolls - a background that scrolls with content reads as a stray
   image, one that holds still reads as the surface the page sits on.

   Two things make it work at this size. It is pinned behind everything with a
   negative z-index on a stacking context created by `body`, and the panels are
   very slightly translucent so the silhouette carries through them instead of
   only showing in the gutters. An earlier attempt kept panels opaque and the
   bird appeared as disconnected crops between cards, which read as noise. */
body { position: relative; z-index: 0; }

/* Layer 1 - the bird itself, pinned behind all content. Sized past the
   viewport height so the wings reach beyond the panel gutters; a small
   centred bird just reads as a stray logo. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  background: url("/static/hawk-bg.png") no-repeat center 46% / auto 118vh;
  opacity: .34;
  pointer-events: none;
  z-index: -1;
}

/* Layer 2 - the same artwork again, this time *over* the panels in `screen`
   blend so the silhouette stays continuous instead of breaking into fragments
   wherever a card sits. Screen is the right operator for neon-on-black line
   art: transparent and dark pixels compose to nothing, so the 71% of this
   image that is empty costs exactly zero, and only the bright strokes lift
   what is beneath them. Kept low, because this layer *can* cross a glyph. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  background: url("/static/hawk-bg.png") no-repeat center 46% / auto 118vh;
  mix-blend-mode: screen;
  opacity: .13;
  pointer-events: none;
  z-index: 40;
}

/* Panels let ~24% of layer 1 through, which is what makes the bird visible
   across the page rather than only between cards. */
.panel, .hero-shell { background-color: rgba(18, 24, 32, .76); }
.cat, .spark-wrap, .item, .driver, .svc, .root {
  background-color: rgba(24, 32, 41, .80);
}

/* The map canvas is the one surface where colour *is* the data - land tint
   carries severity - so a screen-blended wing crossing it would misreport a
   country. Lift only the canvas above layer 2, not its whole panel, so the
   bird still shows across the rest of the hero. */
.worldmap { position: relative; z-index: 50; }

/* Chrome sits above layer 2 rather than under it: a wing crossing the refresh
   button is noise, not branding. Raising the bar also scopes its region menu,
   which stays on 40 inside the bar's own stacking context. The modal backdrop
   is already on 100 - a trace readout is for reading, so it stays clear. */

/* Hidden by default: on a wide screen the watermark is the branding, and a
   second mark in the header alongside it is just clutter. */
.brand-mark-sm {
  display: none;
  height: 30px; width: auto; flex-shrink: 0;
  filter: drop-shadow(0 0 6px rgba(88,166,255,.45));
}

/* A phone cannot use the watermark for branding. Panels stack edge to edge so
   layer 1 has almost no gutter to show through, and layer 2 - which paints
   over panels - is exactly where legibility suffers on a small screen.
   Hiding both, as this rule first did, left the page with no hawk anywhere.
   So the mark returns to the header, where branding is always visible, and
   only the quiet behind-panels layer stays. */
@media (max-width: 760px) {
  .brand-mark-sm { display: block; }

  /* The header carried a brand, a region picker, a timestamp and two buttons
     on one row. At 390px that wrapped the region name onto two lines and
     pushed the buttons off the right edge. Let it wrap deliberately instead,
     and let the region picker take the space it needs. */
  .topbar { flex-wrap: wrap; gap: 8px 12px; padding: 10px 14px; }
  .brand { flex: 1 1 100%; gap: 8px; }
  .brand h1 { font-size: 15px; }
  .region-picker { margin-left: auto; }
  .region { font-size: 12px; padding: 3px 8px 3px 10px; }
  .topbar-actions { flex: 1 1 100%; justify-content: flex-start; gap: 10px; }
  .updated { font-size: 11px; }

  body::after { display: none; }
  body::before {
    opacity: .2;
    background-position: center 26%;
    background-size: auto 44vh;
  }
  .panel, .hero-shell { background-color: rgba(18, 24, 32, .90); }
  .cat, .spark-wrap, .item, .driver, .svc, .root {
    background-color: rgba(24, 32, 41, .92);
  }
}

/* Readers who have asked for less transparency or more contrast get neither
   layer, and fully opaque panels. Branding is never worth someone's ability to
   read the page. */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .brand-mark-sm { display: block; }
  body::before, body::after { display: none; }
  .panel, .hero-shell { background-color: var(--bg-panel); }
  .cat, .spark-wrap, .item, .driver, .svc, .root { background-color: var(--bg-raised); }
}

/* The mascot fronts the map's loading state instead of a bare line of text. */
.map-message { flex-direction: column; gap: 12px; }
.map-message-mark {
  width: 96px; height: auto; opacity: .5;
  filter: drop-shadow(0 0 10px rgba(88,166,255,.35));
}

/* A quiet mark in the footer closes the page with the brand. */
footer .footer-mark {
  height: 22px; width: auto; vertical-align: -6px; margin-right: 8px;
  opacity: .65; filter: drop-shadow(0 0 5px rgba(88,166,255,.35));
}
