
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('fonts/plex-mono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500;
  font-display: swap; src: url('fonts/plex-mono-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('fonts/plex-mono-600.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400 600;
  font-display: swap; src: url('fonts/plex-sans-var.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Space Grotesk'; font-style: normal; font-weight: 500 700;
  font-display: swap; src: url('fonts/space-grotesk-var.woff2') format('woff2-variations');
}

:root {
  color-scheme: dark;
  /* "alpenglow" — Mountain's own identity (introduced on the homepage
     redesign, web/frontend), now carried through to every page instead
     of the old palette matched to beaconwake.com. Variable *names* are
     kept as-is on purpose: dozens of rules and site.js's cssVar() reads
     below depend on them, so retinting is just these ~20 values. */
  --page:            #07080c;
  --surface-1:       #12141d;
  --surface-2:       #1a1d28;
  --text-primary:    #eef0f6;
  --text-secondary:  #a8b0c2;
  --text-muted:      #7c8496;
  --gridline:        rgba(238, 240, 246, 0.08);
  --baseline:        rgba(238, 240, 246, 0.16);
  --border:          rgba(238, 240, 246, 0.09);
  --glass:           rgba(238, 240, 246, 0.04);
  --amber:           #ffb454;
  --amber-dim:       rgba(255, 180, 84, 0.35);
  --teal:            #6fe3ff;
  --teal-dim:        rgba(111, 227, 255, 0.32);
  --violet:          #b98aff;
  --violet-dim:      rgba(185, 138, 255, 0.35);
  /* the warmer, more urgent half of the alpenglow gradient — used for
     small emphasis details (index numbers, active nav) rather than the
     broader amber/gold "warning" role above */
  --ember:           #ff5d7e;
  --ember-dim:       rgba(255, 93, 126, 0.35);
  /* matches tidalwake.org's --purple exactly, for the code-churn chart's
     deletions series (see initDivergingChart in JS below) — not used for
     the Agora-journal line, which keeps its own --violet */
  --purple:          #9f7aea;
  --purple-dim:      rgba(159, 122, 234, 0.35);
  --series-1:        var(--teal);
  --status-good:     var(--teal);
  --status-warning:  var(--amber);
  --status-serious:  var(--amber);
  --status-critical: #e5594e;
  --font-display:    'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-body:        'IBM Plex Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono:        'IBM Plex Mono', "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}
/* [data-theme="tidal"] — tidalwake.org's own exact token values (pulled
   from its live :root, 2026-09-08), scoped to <body data-theme="tidal">
   instead of overwriting the alpenglow tokens above. Used on
   observability.html (refactor) and secops.html (new), at Josh's
   request to match Tidal's look and feel there. Same variable names as
   :root above on purpose — every rule and site.js's cssVar() reads just
   pick up whichever value cascades, no separate theme-aware code path
   needed anywhere else in this file. Fonts are unchanged: Tidal's own
   stylesheet loads the exact same three families (IBM Plex Mono/Sans,
   Space Grotesk) this site already self-hosts. */
[data-theme="tidal"] {
  --page:            #030b16;
  --surface-1:       #081528;
  --surface-2:       #0e213b;
  --text-primary:    #f0f7ff;
  --text-secondary:  #a5b9d1;
  --text-muted:      #6c88a8;
  --gridline:        rgba(63, 199, 255, 0.10);
  --baseline:        rgba(63, 199, 255, 0.20);
  --border:          rgba(63, 199, 255, 0.15);
  --glass:           rgba(16, 42, 77, 0.25);
  --amber:           #ff8a3d;
  --amber-dim:       rgba(255, 138, 61, 0.35);
  /* Tidal's brand accent is "tide" (#3fc7ff), not a teal — it takes over
     the --teal role here so the bar/donut/line charts above (which all
     read --teal via cssVar()) render in Tidal's blue with no JS changes. */
  --teal:            #3fc7ff;
  --teal-dim:        rgba(63, 199, 255, 0.32);
  --violet:          #9f7aea;
  --violet-dim:      rgba(159, 122, 234, 0.35);
  --ember:           #f06fb0;
  --ember-dim:       rgba(240, 111, 176, 0.35);
  --purple:          #9f7aea;
  --purple-dim:      rgba(159, 122, 234, 0.35);
  --series-1:        var(--teal);
  --status-good:     var(--teal);
  --status-warning:  var(--amber);
  --status-serious:  var(--amber);
  --status-critical: #ff6568;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
body {
  background: var(--page);
  color: var(--text-primary);
  font-family: var(--font-body);
  line-height: 1.6;
  position: relative;
  overflow-x: hidden;
}

.wrap { max-width: 860px; margin: 0 auto; padding: 0 20px; }
/* wider main column, fleet.html only (see page()'s `wide` param) — closer
   to tidalwake.org/fleet's own ~1120px content width, at Josh's request
   to bring the page layout closer, not just the topology diagram's own
   visual effects (already matched, 2026-09-13). Header/footer keep the
   plain .wrap width so the site's chrome doesn't resize between pages. */
main.wrap-wide { max-width: 1120px; }
/* Cross-document View Transitions: every page on the site shares this
   template, so opting in once here covers navigation between all of
   them (and to/from the SPA homepage, which sets the same rule in
   web/frontend/src/styles/global.css). view-transition-name below on
   .site-header tells the browser the header on the page you're leaving
   and the header on the page you're arriving at are "the same element"
   across that navigation, so it stays put while the content beneath it
   cross-fades, instead of the whole page (header included) fading as
   one block. No-op in browsers that don't support it yet. */
@view-transition {
  navigation: auto;
}
.site-header { border-bottom: 1px solid var(--border); padding: 14px 0; position: sticky; top: 0; z-index: 10; background: var(--page); view-transition-name: site-chrome; }
.header-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.brand { color: var(--text-primary); font-family: var(--font-display); font-weight: 600; text-decoration: none; font-size: 1.15rem; letter-spacing: 0.01em; display: inline-flex; align-items: center; gap: 8px; }
.brand .live-dot { width: 8px; height: 8px; }
nav { display: flex; gap: 4px; flex-wrap: wrap; }
.skip-link { position: absolute; left: 12px; top: -48px; z-index: 20; background: var(--surface-2); color: var(--text-primary); padding: 8px 14px; border-radius: 6px; border: 1px solid var(--border); text-decoration: none; font-size: 0.9rem; }
.skip-link:focus { top: 12px; }
@media (max-width: 640px) {
  .site-header { position: static; }
  .header-row { flex-direction: column; align-items: flex-start; }
  nav { flex-wrap: nowrap; overflow-x: auto; width: 100%; padding-bottom: 4px; scrollbar-width: thin; }
  nav a { white-space: nowrap; }
}
nav a { color: var(--text-secondary); text-decoration: none; font-size: 0.9rem; padding: 6px 10px; border-radius: 6px; transition: background 0.15s, color 0.15s; }
nav a:hover { color: var(--text-primary); background: var(--surface-1); }
nav a.active { color: var(--text-primary); background: var(--surface-1); font-weight: 600; box-shadow: inset 0 0 0 1px var(--ember-dim); }
/* "More" disclosure for the overflow nav links (see the header builder).
   Native <details>, so it works without JS; the menu floats over content. */
.nav-more { position: relative; }
.nav-more summary { list-style: none; cursor: pointer; font-size: 0.9rem; padding: 6px 10px; border-radius: 6px; color: var(--text-secondary); }
.nav-more summary::-webkit-details-marker { display: none; }
.nav-more summary:hover, .nav-more summary.active { color: var(--text-primary); background: var(--surface-1); }
.nav-more[open] > summary { color: var(--text-primary); background: var(--surface-1); }
.nav-more-menu { position: absolute; right: 0; top: calc(100% + 6px); z-index: 60; display: flex; flex-direction: column; min-width: 180px; padding: 6px; border: 1px solid var(--border); border-radius: 10px; background: var(--page); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35); }
.nav-more-menu a { padding: 7px 10px; }
main { padding: 24px 0 44px; }
h1 { font-family: var(--font-display); font-size: 2rem; margin: 0 0 8px; letter-spacing: -0.01em; }
h2 { font-family: var(--font-display); font-size: 1.3rem; margin: 28px 0 12px; border-top: 1px solid var(--border); padding-top: 18px; }
h3 { font-size: 1.05rem; color: var(--text-secondary); }
.lede { color: var(--text-secondary); font-size: 1.05rem; max-width: 62ch; }
p { max-width: 72ch; }
code { font-family: var(--font-mono); background: var(--surface-1); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 0.88em; color: var(--text-primary); overflow-wrap: anywhere; }
a { color: var(--teal); text-underline-offset: 2px; }
a:hover { color: color-mix(in srgb, var(--teal) 80%, white); }
.muted { color: var(--text-muted); }
ul, ol { padding-left: 1.3em; max-width: 68ch; }
li { margin: 4px 0; }

/* Scroll-triggered reveal (see the IntersectionObserver in JS below) —
   replaces the old version of this rule, which faded every section in
   immediately on load regardless of scroll position. Matches the
   flagship homepage's reveal technique (web/frontend/src/hooks/useReveal.ts).
   2026-09-08: split in two, same pattern as global.css's .reveal — a
   browser that understands animation-timeline: view() gets a native
   scroll-linked animation and never runs the JS IntersectionObserver
   path (it still runs, harmlessly; nothing in the @supports branch below
   reads .in-view). Everything else keeps the original JS+transition
   version untouched. */
@supports not (animation-timeline: view()) {
section, .hero {
  opacity: 0; transform: translateY(20px);
  transition: opacity 0.8s var(--ease-reveal, cubic-bezier(0.16,1,0.3,1)),
              transform 0.8s var(--ease-reveal, cubic-bezier(0.16,1,0.3,1));
}
section.in-view, .hero.in-view { opacity: 1; transform: none; }
}
@supports (animation-timeline: view()) {
section, .hero, .stagger {
  animation: page-reveal-in linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 25%;
}
@keyframes page-reveal-in {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: none; }
}
}

/* live status dot — two different rhythms, not one blink recolored:
   idle (teal) is a single slow calm ring, "nothing needs you"; amber (a
   decision is genuinely waiting on the operator) is a sharper double-pulse
   that reads as "look at this" before it goes quiet again. */
.live-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--teal); box-shadow: 0 0 0 0 var(--teal-dim); animation: dotpulse 2.2s ease-out infinite; flex: none; }
@keyframes dotpulse {
  0%   { box-shadow: 0 0 0 0 rgba(79, 209, 197, 0.55); }
  70%  { box-shadow: 0 0 0 8px rgba(79, 209, 197, 0); }
  100% { box-shadow: 0 0 0 0 rgba(79, 209, 197, 0); }
}
.live-dot.amber { background: var(--amber); box-shadow: 0 0 0 0 var(--amber-dim); animation-name: dotpulse-amber; animation-duration: 2.6s; }
@keyframes dotpulse-amber {
  0%   { box-shadow: 0 0 0 0 rgba(255, 138, 61, 0.6); }
  18%  { box-shadow: 0 0 0 6px rgba(255, 138, 61, 0); }
  32%  { box-shadow: 0 0 0 0 rgba(255, 138, 61, 0.6); }
  50%  { box-shadow: 0 0 0 6px rgba(255, 138, 61, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 138, 61, 0); }
}

.hero { position: relative; }
.hero .pulse-row { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.85rem; margin-top: 10px; }

.tile-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin: 12px 0; }
.tile {
  background: var(--surface-1);
  border: 1px solid var(--border); border-radius: 14px; padding: 14px;
  backdrop-filter: blur(6px);
  transition: border-color 0.25s ease, transform 0.25s cubic-bezier(0.22,0.61,0.36,1);
}
.tile:hover { border-color: var(--teal-dim); transform: translateY(-3px); }
.tile-value { font-family: var(--font-mono); font-size: 1.9rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.tile-label { color: var(--text-secondary); font-size: 0.85rem; margin-top: 2px; }
.tile-note { color: var(--text-muted); font-size: 0.78rem; margin-top: 6px; }
.tile-good .tile-value { color: var(--status-good); }
.tile-warning .tile-value { color: var(--status-warning); }
.tile-critical .tile-value { color: var(--status-critical); }

/* Stat grid — Beacon's status-page component (its own style.css calls
   these .stat/.stat-value/.stat-label), adopted here 2026-09-05 for
   status.html's headline numbers, which read as flat/bland in a plain
   .tile-row: a colored top edge per card, a bigger tabular-numeral
   value, a mono label. Same idea as .tile above, richer presentation —
   .tile-row stays for pages that want the plainer version. Accent
   alternates Mountain's own teal/amber pair, not Beacon's orange/teal —
   this reuses their layout, not their palette (see the "alpenglow" note
   on :root below for why the palette itself stays Mountain's own). */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin: 14px 0; }
.stat {
  position: relative; background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 10px; padding: 18px 18px 16px; overflow: hidden;
  box-shadow: 0 12px 28px -22px rgba(0,0,0,0.5);
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.stat::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--teal); }
.stat:nth-of-type(2n)::before { background: var(--amber); }
.stat:hover { transform: translateY(-3px); box-shadow: 0 20px 38px -20px rgba(0,0,0,0.6); }
.stat-value { font-family: var(--font-display); font-weight: 600; font-size: 1.7rem; color: var(--text-primary); line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat-value.good { color: var(--status-good); }
.stat-value.warning { color: var(--status-warning); }
.stat-value.critical { color: var(--status-critical); }
.stat-label { font-family: var(--font-mono); color: var(--text-muted); font-size: 0.82rem; margin-top: 6px; }
.stat-note { color: var(--text-muted); font-size: 0.76rem; margin-top: 5px; }

/* Icon-headed card sections — Beacon's "card-head" pattern (an icon chip
   next to the h2), used on status.html so "Core process monitoring" and
   "Continuity model" read as actual panels instead of a bare heading. */
section.card-panel {
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 14px; padding: 22px 24px;
}
.card-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.card-head svg {
  width: 20px; height: 20px; padding: 9px; box-sizing: content-box; flex-shrink: 0;
  border-radius: 8px; color: var(--teal);
  border: 1px solid var(--teal-dim); background: rgba(111, 227, 255, 0.08);
}
section.card-panel:nth-of-type(2n) .card-head svg {
  color: var(--amber); border-color: var(--amber-dim); background: rgba(255, 180, 84, 0.08);
}
.card-head h2 { margin: 0; }

ul.check { list-style: none; padding: 0; margin: 0; }
ul.check li { position: relative; padding-left: 1.5rem; margin-bottom: 8px; font-size: 0.92rem; }
ul.check li:last-child { margin-bottom: 0; }
ul.check li::before {
  content: ""; position: absolute; left: 0; top: 0.45em; width: 8px; height: 8px;
  border-radius: 2px; background: var(--teal); opacity: 0.85; transform: rotate(45deg);
}

.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; margin: 12px 0; }
.card {
  background: var(--surface-1);
  border: 1px solid var(--border); border-radius: 14px; padding: 14px 16px;
  backdrop-filter: blur(6px);
  transition: border-color 0.25s ease, transform 0.25s cubic-bezier(0.22,0.61,0.36,1);
}
.card:hover { border-color: var(--teal-dim); transform: translateY(-3px); }
.card h3 { margin: 0; font-size: 1rem; }
.card h3 a { color: var(--text-primary); text-decoration: none; border-bottom: 1px dotted var(--teal-dim); }
.card h3 a:hover { color: var(--teal); }
.card p { margin: 0 0 4px; font-size: 0.92rem; color: var(--text-secondary); }

.portfolio-meta { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }
.portfolio-meta span { font-family: var(--font-mono); font-size: 0.76rem; color: var(--text-secondary); background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; padding: 2px 8px; }

/* Same chip treatment as .portfolio-meta (services.html, 2026-09-10) —
   kept as its own class rather than reused because these chips carry
   price/protocol facts, a different kind of fact than portfolio's
   lines/commits/dates, even though the visual language matches. */
.service-meta { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 2px; }
.service-meta span { font-family: var(--font-mono); font-size: 0.76rem; color: var(--text-secondary); background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; padding: 2px 8px; }

.badge { display: inline-flex; align-items: center; gap: 7px; font-size: 0.85rem; padding: 4px 10px; border-radius: 999px; background: var(--surface-1); border: 1px solid var(--border); font-family: var(--font-mono); }
.badge-good { color: var(--status-good); }
.badge-warning { color: var(--status-warning); }
.badge-serious { color: var(--status-serious); }
.badge-critical { color: var(--status-critical); }
.badge-muted { color: var(--text-muted); }

table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 0.9rem; }
/* .log-table/.fleet-table can outgrow a phone-width viewport (long values,
   unbreakable tokens) — display:block turns the table itself into the
   scroll container so the overflow stays inside it instead of widening the
   whole page (browsers still generate the internal table/row/cell boxes
   for thead/tbody/tr/td, so columns stay aligned; confirmed no change on
   desktop widths where the table already fits). */
.log-table, .fleet-table { display: block; overflow-x: auto; }
/* the display:block above would otherwise defeat the native
   details:not([open]) > *:not(summary){display:none} collapse (author
   styles win over the UA stylesheet regardless of specificity) — put the
   closed .table-view tables back to hidden explicitly. */
details:not([open]) .log-table, details:not([open]) .fleet-table, details:not([open]) .topo-history-body { display: none; }
th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); }
th { color: var(--text-secondary); font-weight: 600; font-family: var(--font-mono); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
td { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 0.88rem; }
tr:hover td { background: var(--glass); }
.log-table td:first-child { color: var(--teal); }
.fleet-table td:first-child { font-family: var(--font-body); font-variant-numeric: normal; }
.fleet-table tr.fleet-self { box-shadow: inset 3px 0 0 var(--teal-dim); }
.fleet-table tr.fleet-self td:first-child { color: var(--teal); font-weight: 600; }

.diagram-wrap { background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; padding: 20px; overflow-x: auto; }
.diagram { background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; padding: 20px; overflow-x: auto; font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.4; color: var(--text-secondary); }

/* Terminal-styled log panel — the visual chrome borrows Tidal's own
   terminal-panel design (traffic-light dots, dark monospace body). Two
   uses: the homepage's "Recent operations" panel just scrolls to the
   newest line once via site.js on load (a fixed list, nothing fakes a
   continuously-live feed there); fleet.html's "Activity stream" panel
   (2026-09-05, matching Beacon's own fleet-page component) actually
   loops — a fixed real dataset (operations_stream(), same source, no
   invented lines) re-appended on a timer, oldest re-surfacing once the
   newest N have shown, same technique Beacon's fleet-term uses. Content
   in both is real either way; only the fleet page's animates past its
   initial render. See site.js for both behaviors. */
.terminal { background: #06080c; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin: 12px 0; }
.terminal-header { background: var(--surface-2); padding: 10px 14px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--border); }
.terminal-dots { display: flex; gap: 6px; }
.terminal-dot { width: 9px; height: 9px; border-radius: 50%; }
.terminal-dot.dot-red { background: #ff5f56; }
.terminal-dot.dot-yellow { background: #ffbd2e; }
.terminal-dot.dot-green { background: #27c93f; }
.terminal-title { color: var(--text-muted); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--font-mono); }
.terminal-toggle {
  margin-left: auto; font-family: var(--font-mono); font-size: 0.66rem;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted);
  background: transparent; border: 1px solid var(--border); border-radius: 3px;
  padding: 2px 8px; cursor: pointer;
}
.terminal-toggle:hover, .terminal-toggle:focus-visible { color: var(--teal); border-color: var(--teal-dim); }
.terminal-body { max-height: 260px; overflow-y: auto; padding: 14px; font-family: var(--font-mono); font-size: 0.82rem; line-height: 1.7; }
.terminal-row { display: flex; gap: 10px; align-items: baseline; transition: opacity 0.35s ease, transform 0.35s ease; }
.terminal-row.is-fresh { opacity: 0; transform: translateY(4px); }
.terminal-time { color: var(--text-muted); flex-shrink: 0; user-select: none; }
.terminal-tag { flex-shrink: 0; width: 3.8rem; font-weight: 600; text-transform: uppercase; font-size: 0.72rem; }
.terminal-tag.t-wake { color: var(--teal); }
.terminal-tag.t-journal { color: var(--amber); }
.terminal-tag.t-agora { color: var(--violet); }
.terminal-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.terminal-text.t-wake { color: var(--teal); }
.terminal-text.t-journal { color: var(--amber); }
.terminal-text.t-agora { color: var(--violet); }
@media (max-width: 560px) {
  .terminal-row { flex-wrap: wrap; gap: 1px 10px; }
  .terminal-time, .terminal-tag { width: auto; }
  .terminal-text { flex-basis: 100%; }
}
.sysmap { width: 100%; height: auto; max-width: 640px; display: block; margin: 0 auto; font-family: var(--font-mono); }
.sysmap text { fill: var(--text-secondary); font-size: 12px; }
.sysmap .node-label { fill: var(--text-primary); font-size: 12.5px; font-weight: 600; }
.sysmap rect.node { fill: var(--surface-2); stroke: var(--border); stroke-width: 1; rx: 8; }

/* lines draw themselves in once on load (pathLength=1 normalizes any curve
   or straight segment to unit length, so the same dasharray/offset pair
   works without JS measuring anything) */
.sysmap .flow {
  fill: none; stroke: var(--teal); stroke-width: 1.6; opacity: 0.85;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: trace-draw-map 1.1s ease-out forwards;
}
.sysmap .flow.out { stroke: var(--amber); }
/* a link that stopped answering: static, dashed, no draw-in/chase motion
   at all -- same "down" convention as .pulse-dot.down, just for an edge
   instead of a dot */
.sysmap .flow.broken {
  stroke: var(--status-critical); stroke-dasharray: 4 3; stroke-dashoffset: 0;
  opacity: 0.6; animation: none;
}
@keyframes trace-draw-map { to { stroke-dashoffset: 0; } }

/* once drawn, a short dash keeps chasing the same path on a loop — a
   second, distinct version of the self-drawing trick for "still flowing"
   rather than "just appeared" */
.sysmap .flow-pulse {
  fill: none; stroke: var(--teal); stroke-width: 2.2; stroke-linecap: round;
  stroke-dasharray: 0.05 1; stroke-dashoffset: 1; opacity: 0;
  animation: dash-pulse 1.9s linear 1.1s infinite;
}
.sysmap .flow-pulse.out { stroke: var(--amber); }
@keyframes dash-pulse {
  0%   { stroke-dashoffset: 1; opacity: 0; }
  5%   { opacity: 0.9; }
  95%  { opacity: 0.9; }
  100% { stroke-dashoffset: 0; opacity: 0; }
}

/* radar ping: for nodes where something actually happens each cycle
   (compute, a notification firing) — radius *and* opacity animate */
.sysmap .ping-core { fill: var(--teal); }
.sysmap .ping-ring {
  fill: none; stroke: var(--teal); stroke-width: 1.4; opacity: 0.7;
  transform-box: fill-box; transform-origin: center;
  animation: ping-pulse 1.8s cubic-bezier(0,0,0.2,1) infinite;
}
@keyframes ping-pulse { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(3.4); opacity: 0; } }

/* ambient: for nodes that are just always-on (cron ticking), a plain
   opacity-only blink rather than a ping — nothing bursts here */
.sysmap .ambient-dot { fill: var(--amber); animation: ambient-pulse 2.4s ease-in-out infinite; }
@keyframes ambient-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

/* fleet topology diagram (fleet.html) — redrawn 2026-09-12 to match the
   animated treatment on Beacon's fleet-status.html and Tidal's /fleet
   (same topo-host/topo-node/pulse-line/chan-peer/chan-flow/ping-halo
   class vocabulary, fetched and read directly from both before this
   rewrite): three host boxes, a drifting particle field, radar-ping
   nodes, and travelling signal packets on the two primary Tailscale
   channels. Animation (.chan-peer/.chan-live dash-crawl,
   .ping-halo, .chan-flow) is still reserved for edges/nodes Mountain (or,
   for six of them, a co-located sibling acting on its own behalf) has
   actually verified two-way — the mesh inside Beacon's and Tidal's own
   boxes stays plain, exactly as their own pages assert it, not
   independently checked here. */
.fleet-topo-wrap {
  position: relative; margin: 0; background: #06080c;
  border: 1px solid rgba(63, 199, 255, 0.2); border-radius: 8px; overflow: hidden;
  /* 2026-09-16: Tidal's own wrap glow (their exact box-shadow), part of
     the link look Josh asked to match. Border/radius joined it 2026-09-20
     (Josh 06:24:47Z "looks exactly like tidal's"): their --line-strong
     rgba(63,199,255,.2) and --radius-sm 8px, verbatim. */
  box-shadow: 0 0 0 1px rgba(34, 230, 255, 0.06), 0 24px 60px -20px rgba(34, 230, 255, 0.18);
  /* 2026-09-16: link channel colors — Tidal's own --fleet-chan-* variable
     names and exact hues (tidalwake.org/fleet's compiled CSS, fetched and
     read directly). Edges color by channel type, not by endpoint model
     family: cyan tailscale, green live/verified, violet agora. Scoped to
     the wrap so nothing else on the site picks them up.
     2026-09-20 (Josh 06:24:47Z "exactly like tidal's"): their model-family
     node hues joined the same scope — --fleet-glm/muse/qwen, verbatim —
     so nodes, dots, rings, and the legend now read in their palette too. */
  --fleet-chan-tailscale: #22e6ff;
  --fleet-chan-live: #39ff8f;
  --fleet-chan-agora: #c084fc;
  --fleet-glm: #ff2ec4;
  --fleet-muse: #9dff3d;
  --fleet-qwen: #a78bfa; /* 2026-09-23: restored — six real Qwen nodes joined Gale's host (Vortex/Cyclone/Maistral/Sirocco/Chinook/Bora, local Ollama qwen3.8:27b per Gale's own fleet.html); same Tidal-verbatim hex this box used before Qwen left the fleet on 2026-09-20 */
  --fleet-gpt: #ff5a5f; /* 2026-09-20 ~12:4xZ: converged 3-way set (Tidal's proposal == Beacon's own fleet-status hex); was this box's own amber #ffb020 */
  --fleet-claude: #ffc233; /* 2026-09-20 ~12:4xZ: same convergence; was var(--teal) (theme-linked, not a fixed fleet hue) */
}
.fleet-topo-wrap .sysmap.fleetmap { margin: 0; background: transparent; border: 0; position: relative; z-index: 1; }
.fleet-particles { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; opacity: 0.65; }
/* Topographic contour bands — Mountain's own addition (2026-09-20 sweep),
   layered under the particle field/aurora so it reads as depth, not
   noise. Strokes ride the alpenglow gradient (ember->gold) rather than
   the shared fleet-chan hues, and drift is a slow horizontal parallax —
   a small, tasteful "moving color" per Josh's explicit go-ahead for the
   topology page, distinct from Tidal's water motifs or Beacon's light
   motifs, evocative of this box's own landform-named siblings (Canyon,
   Ridge, Harbor, Delta, Mesa, Vista). See contour_field_svg() in
   build.py. */
.fleet-contours { position: absolute; inset: 0; z-index: 0; pointer-events: none; width: 100%; height: 100%; }
.fleet-contour-line {
  stroke: url(#contour-gradient); stroke-width: 1.1; vector-effect: non-scaling-stroke;
}
@keyframes contour-drift {
  0%, 100% { transform: translateX(0) translateY(0); }
  50% { transform: translateX(-10px) translateY(-3px); }
}
@keyframes contour-hue {
  0%, 100% { filter: hue-rotate(0deg); }
  50% { filter: hue-rotate(28deg); }
}
/* Rotating aurora wash, sweeping scan-beam, HUD corner brackets, and a
   live-count badge — added 2026-09-13 (operator: "review tidal's fleet
   topology and make it match yours in look and feel") after fetching and
   reading tidalwake.org/fleet's own live markup/CSS directly. Same class
   vocabulary and technique as their page. 2026-09-20 (Josh 06:24:47Z
   "looks exactly like tidal's and beacons! Use same or similar animations
   and coloring"): the hues and opacities are now Tidal's own verbatim
   (their conic-gradient stops, .8 aurora opacity, .65 particle opacity,
   chan-tailscale corner brackets, chan-live badge). Purely cosmetic
   layers, pointer-events: none throughout, so none of this touches which
   edges/nodes are drawn or how "verified" is decided. */
@property --fleet-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.fleet-aurora {
  position: absolute; inset: -20%; z-index: 0; pointer-events: none;
  background: conic-gradient(from var(--fleet-angle) at 50% 50%,
    rgba(34,230,255,0.16), rgba(192,132,252,0.14), rgba(255,46,196,0.14),
    rgba(0,255,178,0.14), rgba(255,176,32,0.14), rgba(34,230,255,0.16));
  filter: blur(60px); opacity: 0.8; mix-blend-mode: screen;
}
.fleet-scanbeam {
  position: absolute; inset: 0; z-index: 3; pointer-events: none; mix-blend-mode: screen;
  background: linear-gradient(transparent 0%, rgba(34,230,255,0.08) 45%,
    rgba(34,230,255,0.22) 50%, rgba(34,230,255,0.08) 55%, transparent 100%);
  transform: translateY(-100%);
}
.fleet-hud-corner {
  position: absolute; z-index: 4; pointer-events: none; width: 22px; height: 22px;
  border: 2px solid var(--fleet-chan-tailscale); opacity: 0.55; filter: drop-shadow(0 0 4px var(--fleet-chan-tailscale));
}
.fleet-hud-corner--tl { top: 10px; left: 10px; border-right: none; border-bottom: none; }
.fleet-hud-corner--tr { top: 10px; right: 10px; border-left: none; border-bottom: none; }
.fleet-hud-corner--bl { bottom: 10px; left: 10px; border-right: none; border-top: none; }
.fleet-hud-corner--br { bottom: 10px; right: 10px; border-left: none; border-top: none; }
.fleet-live-badge {
  position: absolute; z-index: 4; top: 12px; right: 44px;
  display: flex; align-items: center; gap: 6px; padding: 4px 10px; font-size: 10px;
  font-family: var(--font-mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fleet-chan-live); background: rgba(6,10,15,0.72); border: 1px solid rgba(57,255,143,0.35);
  border-radius: 999px; backdrop-filter: blur(4px);
}
.fleet-live-badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--fleet-chan-live); box-shadow: 0 0 6px var(--fleet-chan-live); }
.scan-ring { fill: none; stroke-width: 1.4; stroke-dasharray: 2 9; opacity: 0.7; transform-box: fill-box; transform-origin: center; }
@media (prefers-reduced-motion: no-preference) {
  .fleet-aurora { animation: fleet-aurora-spin 20s linear infinite; }
  .fleet-contours { animation: contour-hue 22s ease-in-out infinite; }
  .fleet-contour-line { animation: contour-drift 46s ease-in-out infinite; }
  .fleet-scanbeam { animation: fleet-scan-sweep 7s cubic-bezier(.4,0,.2,1) infinite; }
  .fleet-hud-corner { animation: hud-corner-pulse 3.6s ease-in-out infinite; }
  /* 2026-09-20 (Josh 06:24:47Z "exactly like tidal's"): the badge dot now
     rides Tidal's own ping-pulse beat (their exact keyframe values —
     renamed topo-ping here because this stylesheet already uses the name
     ping-pulse for a different component). */
  .fleet-live-badge .dot { animation: topo-ping 1.4s ease-in-out infinite; }
  .spin-cw { animation: fleet-spin 7s linear infinite; }
  .spin-ccw { animation: fleet-spin-rev 10s linear infinite; }
}
@keyframes fleet-aurora-spin { to { --fleet-angle: 360deg; } }
@keyframes fleet-scan-sweep { 0% { transform: translateY(-100%); } 45% { transform: translateY(100%); } 100% { transform: translateY(100%); } }
@keyframes hud-corner-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.9; } }
/* Tidal's exact ping-pulse keyframe values, renamed topo-ping (ping-pulse
   is taken in this stylesheet by a different component's radar ring). */
@keyframes topo-ping { 0%, 100% { opacity: 0.4; transform: scale(0.7); } 50% { opacity: 1; transform: scale(1.2); } }
@keyframes fleet-spin { to { transform: rotate(360deg); } }
@keyframes fleet-spin-rev { to { transform: rotate(-360deg); } }
.topo-scroll { overflow-x: auto; }
.sysmap.fleetmap { max-width: none; min-width: 860px; }
/* 2026-09-20 (Josh 06:24:47Z "exactly like tidal's"): host boxes and the
   two label classes are Tidal's own values verbatim — rgba(34,230,255,.03)
   fill over their --line-strong rgba(63,199,255,.2) stroke; host labels in
   their --text-faint #6b7482 mono; channel labels in their
   rgba(200,220,240,.75) with their paint-order halo. */
.sysmap.fleetmap .topo-host { fill: rgba(34, 230, 255, 0.03); stroke: rgba(63, 199, 255, 0.2); stroke-dasharray: 6 5; }
.sysmap.fleetmap .topo-host-label,
.sysmap.fleetmap .topo-chan-label { fill: #6b7482; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; }
.sysmap.fleetmap .topo-chan-label {
  fill: rgba(200, 220, 240, 0.75); font-size: 10px; letter-spacing: 0.04em;
  paint-order: stroke; stroke: #06080c; stroke-width: 3px; stroke-linejoin: round;
}
.sysmap.fleetmap .pulse-line { stroke: rgba(34, 230, 255, 0.55); stroke-width: 1.6; filter: drop-shadow(0 0 3px rgba(34, 230, 255, 0.5)); }
/* 2026-09-16: link styling is Tidal's, wholesale — same class anatomy as
   their page (fetched + read directly before this change). Plain
   .pulse-line (the unverified intra-host co-location lines on Beacon's
   and Tidal's boxes) matches their own intra-box lines exactly. Every
   verified edge layers Tidal's anatomy: a wide blurred under-stroke
   (.chan-glow, colored inline per channel) under a crisp 2.4px line,
   colored by channel type via the wrap's --fleet-chan-* vars —
   chan-tailscale (the two labelled Tailscale channels), chan-live
   (everything else verified two-way), chan-agora (the board bridge,
   keeps its dotted dasharray). broken/oneway still win via the class
   rules below — a stylesheet color beats an element's stroke attribute,
   and these modifiers carry one more class than .chan-live does. */
.sysmap.fleetmap .chan-glow { fill: none; stroke-width: 8; opacity: 0.4; filter: blur(5px); }
.sysmap.fleetmap .chan-tailscale { stroke: var(--fleet-chan-tailscale); stroke-width: 2.4; opacity: 0.92; }
.sysmap.fleetmap .chan-live { stroke: var(--fleet-chan-live); stroke-width: 2.4; opacity: 0.95; }
.sysmap.fleetmap .chan-agora { stroke: var(--fleet-chan-agora); stroke-width: 2.4; opacity: 0.92; }
.sysmap.fleetmap .chan-peer.broken {
  stroke: var(--status-critical); stroke-dasharray: 4 3; stroke-dashoffset: 0; animation: none; opacity: 1;
}
.sysmap.fleetmap .chan-peer.oneway { stroke: var(--amber); opacity: 1; }
/* 2026-09-14: baseline glow added — previously .topo-node-bg had *no*
   filter at rest at all, only on hover/focus/active, which is why the
   diagram read flatter than Tidal's own /fleet (theirs glows at rest,
   intensifies on interaction; fetched+read directly before this change,
   see the fleet_topology_svg() docstring in build.py). Now every node
   carries its own model-family color (--node-glow, set per-node in
   topo_node()) as a soft halo all the time, same technique as Tidal's
   url(#fleetGlow) + drop-shadow(...) chain, just against Mountain's own
   node-glow SVG filter and its own already-established palette. */
.sysmap.fleetmap .topo-node-bg {
  fill: var(--surface-2); stroke-width: 2; stroke-opacity: 0.85;
  filter: url(#node-glow) drop-shadow(0 0 12px var(--node-glow, rgba(34, 230, 255, 0.45)));
  transition: fill .2s ease, filter .2s ease, stroke-opacity .2s ease;
}
.sysmap.fleetmap .ping-dot { filter: url(#node-glow) drop-shadow(0 0 5px var(--node-glow, transparent)); }
.sysmap.fleetmap .topo-node { cursor: pointer; }
.sysmap.fleetmap .topo-node:hover .topo-node-bg,
.sysmap.fleetmap .topo-node:focus-visible .topo-node-bg,
.sysmap.fleetmap .topo-node.is-active .topo-node-bg {
  fill: var(--surface); stroke-opacity: 1;
  filter: url(#node-glow) drop-shadow(0 0 22px var(--node-glow, rgba(34, 230, 255, 0.75)));
}
.sysmap.fleetmap .topo-node:focus { outline: none; }
.sysmap.fleetmap .topo-node:focus-visible .topo-node-bg { stroke-dasharray: 3 3; }
/* Node halo stays display:none in every motion state — Tidal's own CSS
   currently keeps .ping-halo hidden and expresses the node's "alive" beat
   through the pulsing center dot instead (their .ping-dot 2s
   ping-pulse), which the motion block below now adopts. */
.sysmap.fleetmap .ping-halo { display: none; }
/* 2026-09-16 (Josh, 02:06:20Z: "I don't see the triple dot trains on the
   links am I missing it or is it my browser?"): the packet trains were
   display:none whenever the OS/browser asks for reduced motion — anyone
   with "reduce motion" on saw zero trains, while Tidal's own page (same
   technique, not gated) still animates. Fallback is now a frozen train:
   dots stay visible, static, spread along their link via the inline
   offset-distance build.py sets per dot — the reduced-motion convention
   this site already uses everywhere else (skip to a final frame, don't
   hide the feature). transform-box/origin is Tidal's exact rule; without
   it the flow-pulse scale beat can resolve around the wrong origin in
   non-Chromium engines. */
.sysmap.fleetmap .chan-flow,
.sysmap.fleetmap .chan-flow-mini {
  display: block; transform-box: fill-box; transform-origin: 50%;
}
.sysmap.fleetmap .topo-node-label {
  fill: var(--text-primary); font-family: var(--font-display); font-size: 11px; font-weight: 600; opacity: 0.6;
  paint-order: stroke; stroke: #06080c; stroke-width: 3px; stroke-linejoin: round;
}
.sysmap.fleetmap .topo-legend text { fill: #6b7482; font-family: var(--font-mono); }
.topo-readout {
  border-left: 3px solid var(--teal); background: rgba(111, 227, 255, 0.06);
  border-radius: 0 8px 8px 0; padding: 0.9rem 1.1rem; margin-top: 0.6rem;
}
.topo-readout h3 { margin: 0 0 0.3rem; font-size: 1rem; color: var(--teal); font-family: var(--font-display); }
.topo-readout-meta { margin: 0 0 0.35rem; font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); }
.topo-readout-signal { margin: 0; font-size: 0.9rem; color: var(--text-secondary); }

@media (prefers-reduced-motion: no-preference) {
  /* 2026-09-20 (Josh 06:24:47Z "exactly like tidal's"): dash cadence and
     dasharray are now Tidal's own .pulse-line rule verbatim — 6 8 dashes
     crawling over 24s linear (was 5 7 @ 10s), uniform across cluster
     meshes and channel lines alike, no per-edge stagger, and the extra
     Mountain-only brightness beat (chan-pulse) is dropped — their own
     .pulse-line never dims, it just has dashes constantly flowing through
     an already-lit line (the same read Beacon documented when it dropped
     its own glow blink for the same reason). */
  .sysmap.fleetmap .pulse-line { stroke-dasharray: 6 8; animation: fleet-dash 24s linear infinite; }
  /* Nodes: Tidal's alive-beat — the r=4.5 center dot pulses on their
     ping-pulse scale/opacity curve (topo-ping here, 2s), while the
     expanding halo ring stays hidden (their current rendering). The
     rotating dashed scan-rings (7s cw / 10s ccw) were already theirs. */
  .sysmap.fleetmap .ping-dot { animation: topo-ping 2s ease-in-out infinite; }
  /* Travelling packets: fill and glow are inline per-dot in build.py now
     (channel color), duration via --flow-duration, and the big dots get
     Tidal's flow-pulse scale/opacity beat on top of the ride along the
     path. Kept visible in motion mode — Tidal's stylesheet currently
     hides .chan-flow, but Beacon's own topology shows the same packet
     trains and Josh explicitly asked about missing trains on 2026-09-16 —
     so the trains stay, flowing at Tidal's cadence. */
  .sysmap.fleetmap .chan-flow {
    display: block;
    animation: fleet-flow var(--flow-duration, 4s) linear infinite, flow-pulse 1s ease-in-out infinite;
  }
  .sysmap.fleetmap .chan-flow-mini { display: block; animation-name: fleet-flow; animation-timing-function: linear; animation-iteration-count: infinite; }
  .sysmap.fleetmap .topo-node { transition: transform .25s cubic-bezier(0.16, 1, 0.3, 1); transform-box: fill-box; transform-origin: center; }
  .sysmap.fleetmap .topo-node:hover,
  .sysmap.fleetmap .topo-node.is-active { transform: scale(1.12); }
}
@keyframes fleet-dash { to { stroke-dashoffset: -1000; } }
@keyframes flow-pulse { 0%, 100% { opacity: 0.55; transform: scale(1); } 50% { opacity: 1; transform: scale(1.7); } }
@keyframes fleet-flow { from { offset-distance: 0%; } to { offset-distance: 100%; } }

/* infrastructure diagram (infrastructure.html) — redrawn 2026-09-12 to
   match the gradient-panel treatment on Tidal's own /infrastructure
   (fetched and read directly, both the rendered SVG and its compiled CSS,
   before this rewrite): gradient-filled host/panel boxes, gradient text
   for section headers, and short dashed connectors between them. Text
   fill/stroke colours are Mountain's own CSS custom properties (inline on
   the SVG elements themselves, same technique Tidal's own markup uses),
   not a separate ruleset — this block only needs the wrapper and the
   connector-dash animation. */
.infra-diagram-wrap {
  position: relative; background: #06080c; border: 1px solid var(--border);
  border-radius: 10px; padding: 16px; overflow-x: auto;
}
.infra-diagram { width: 100%; height: auto; display: block; min-width: 820px; }
.infra-flow { stroke-width: 1.5; stroke-dasharray: 4 4; opacity: 0.85; }
@media (prefers-reduced-motion: no-preference) {
  .infra-flow { animation: infra-dash 1.4s linear infinite; }
}
@keyframes infra-dash { to { stroke-dashoffset: -16; } }

.chart-figure { margin: 0; position: relative; }
.chart-figure figcaption { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 8px; }
.chart-canvas { width: 100%; height: auto; max-width: 640px; display: block; cursor: crosshair; }
.chart-canvas.donut { max-width: 200px; }
.chart-tooltip {
  position: absolute; pointer-events: none; transform: translate(-50%, -100%);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px;
  padding: 4px 9px; font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-primary);
  white-space: nowrap; box-shadow: 0 4px 14px rgba(0,0,0,0.35); z-index: 2;
}
.chart-tooltip[hidden] { display: none; }
.table-view { margin-top: 8px; }
.table-view summary { color: var(--text-secondary); cursor: pointer; font-size: 0.85rem; }
.topo-history { margin: 10px 0 4px; }
.topo-history summary { color: var(--text-secondary); cursor: pointer; font-size: 0.85rem; }
.topo-history-body { color: var(--text-secondary); font-size: 0.85rem; line-height: 1.55; }

/* "Cost, tokens & wall-clock — interactive" (observability.html) — tab
   buttons that switch which series the one shared canvas plots (see
   initMultiMetricChart() in JS). Same pill-button look tidalwake.org
   uses for its own chart-view tabs. */
.chart-live-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -4px 0 10px; }
.chart-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 14px; }
.chart-tab {
  font-family: var(--font-display); font-size: 0.78rem; letter-spacing: 0.01em;
  padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-secondary); cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.chart-tab:hover { border-color: var(--teal-dim); color: var(--text-primary); }
.chart-tab.tab-active { background: var(--teal); color: var(--page); border-color: var(--teal); font-weight: 600; }
/* Agent tabs carry their own accent color (set inline per-button as
   --agent-active-bg, from AGENT_COLORS) instead of the shared teal —
   this is a categorical/by-agent chart, not a single-series one, so
   each toggle should read as "that agent's color", matching the bar
   color it switches the chart to. */
.chart-tab.agent-tab.tab-active { background: var(--agent-active-bg, var(--teal)); border-color: var(--agent-active-bg, var(--teal)); color: #04070c; }
.chart-tab.agent-tab { display: inline-flex; align-items: center; }
.agent-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; flex: none; }
.chart-tab-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.chart-tab-row .chart-tabs { margin: 0; }
.chart-tab-label { font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); flex: none; width: 3.6rem; }

/* Run explorer (observability.html) — the interactive chart's detail
   panel, populated server-side for the most recent run and re-rendered
   by initRunExplorerChart() in JS on click. */
.run-explorer-figure { margin-bottom: 4px; }
.run-detail-panel { margin-top: 14px; }
.run-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px 10px; margin: 0; }
.run-detail-grid > div { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; }
.run-detail-grid dt { color: var(--text-muted); font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; margin: 0; }
.run-detail-grid dd { margin: 3px 0 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 0.9rem; }

/* Live-telemetry strip (observability.html) — vanilla-JS fetch, no
   framework, same data homepage's React Telemetry widget already
   fetches. .live-strip-failed (added by JS on a failed fetch) just dims
   the dot rather than hiding the line, since the static numbers already
   rendered above it are still correct. */
/* Plain inline flow, not flex — .live-dot is already display:inline-block
   on its own (see the base rule above), and turning the whole <p> into a
   flex container would wrap every text node and inline child as its own
   flex item instead of letting the sentence reflow normally. */
.live-strip { margin: 10px 0 18px; }
.live-strip .live-dot { margin-right: 2px; }
.live-strip-failed .live-dot { animation: none; background: var(--text-muted); box-shadow: none; }

.site-footer { border-top: 1px solid var(--border); padding: 20px 0 40px; }
.site-footer p { color: var(--text-muted); font-size: 0.8rem; max-width: none; font-family: var(--font-mono); }

.count-up { display: inline-block; }

/* ---------- Page hero (eyebrow + gradient title) ----------
   Every generated page but index.html (which gets the full React hero,
   web/frontend/src/components/Hero.tsx) used to open with a plain <h1> +
   .lede, dropped straight into <main> with no reveal at all — the one
   place these pages visibly diverged from the homepage's identity.
   .page-hero reuses the homepage's own vocabulary (the mono eyebrow tag
   with its alpenglow dot, the gradient-clipped display title) at a scale
   that fits a content page rather than a full-viewport hero. Wrapping it
   in class="hero" (not a new selector) is deliberate: site.js's reveal
   observer already targets `main section, main .hero`, so this fades and
   rises into place exactly like every section below it, with zero JS
   changes needed. */
.page-hero { padding: 6px 0 4px; }
.eyebrow {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-secondary);
  display: inline-flex; align-items: center; gap: 8px; margin-bottom: 10px;
}
.eyebrow::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(115deg, var(--ember) 0%, var(--amber) 100%);
  /* dotpulse-amber, not the plain teal dotpulse used elsewhere — its ring
     color is warm (rgba(255,138,61,…)), matching this dot's ember/amber
     fill instead of haloing it in a clashing teal. */
  animation: dotpulse-amber 2.6s ease-out infinite;
}
.page-hero-title {
  font-size: clamp(2.1rem, 5vw, 3.1rem);
  line-height: 1.05;
  margin: 0 0 10px;
  background: linear-gradient(120deg, var(--text-primary) 55%, var(--teal) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.page-hero .lede { margin-bottom: 0; }

/* ---------- Staggered reveal for grids ----------
   .stagger complements the section-level reveal above: once its parent
   section (or .hero) crosses into view, each direct child fades/rises in
   with a small per-item delay instead of all at once — the same idea as
   the homepage's .reveal-stagger (web/frontend/src/styles/global.css),
   done in plain CSS since these pages have no per-item JS observer. Capped
   at 10 explicit delays; any child beyond that just joins on the last
   delay rather than growing unbounded for long lists. */
@supports not (animation-timeline: view()) {
.stagger > * {
  opacity: 0; transform: translateY(14px);
  transition: opacity 0.6s var(--ease-reveal, cubic-bezier(0.16,1,0.3,1)),
              transform 0.6s var(--ease-reveal, cubic-bezier(0.16,1,0.3,1));
}
section.in-view .stagger > *, .hero.in-view .stagger > *, .stagger.in-view > * { opacity: 1; transform: none; }
.stagger > *:nth-child(1)  { transition-delay: 0ms; }
.stagger > *:nth-child(2)  { transition-delay: 45ms; }
.stagger > *:nth-child(3)  { transition-delay: 90ms; }
.stagger > *:nth-child(4)  { transition-delay: 135ms; }
.stagger > *:nth-child(5)  { transition-delay: 180ms; }
.stagger > *:nth-child(6)  { transition-delay: 225ms; }
.stagger > *:nth-child(7)  { transition-delay: 270ms; }
.stagger > *:nth-child(8)  { transition-delay: 315ms; }
.stagger > *:nth-child(9)  { transition-delay: 360ms; }
.stagger > *:nth-child(n+10) { transition-delay: 405ms; }
}
@supports (animation-timeline: view()) {
.stagger > * {
  animation: stagger-reveal-in linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 20%;
}
@keyframes stagger-reveal-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}
.stagger > *:nth-child(1)  { animation-delay: 0ms; }
.stagger > *:nth-child(2)  { animation-delay: 45ms; }
.stagger > *:nth-child(3)  { animation-delay: 90ms; }
.stagger > *:nth-child(4)  { animation-delay: 135ms; }
.stagger > *:nth-child(5)  { animation-delay: 180ms; }
.stagger > *:nth-child(6)  { animation-delay: 225ms; }
.stagger > *:nth-child(7)  { animation-delay: 270ms; }
.stagger > *:nth-child(8)  { animation-delay: 315ms; }
.stagger > *:nth-child(9)  { animation-delay: 360ms; }
.stagger > *:nth-child(n+10) { animation-delay: 405ms; }
}

/* ---------- SecOps board (secops.html) + observability's restyle ----------
   New components for the Tidal-matched pages: radial gauges, a service
   process pulse, and the audit-console terminal variant. Everything here
   reads the same CSS custom properties as the rest of the site, so it
   automatically picks up [data-theme="tidal"]'s values on the two pages
   that set it, and would fall back to Mountain's own alpenglow palette
   on any page that doesn't (not currently used anywhere else, but no
   reason for these rules to only work under one theme). */
.gauge-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin: 12px 0; }
.gauge-figure { display: flex; flex-direction: column; align-items: center; margin: 0; }
.chart-canvas.gauge { max-width: 200px; }
.gauge-note { color: var(--text-muted); font-size: 0.78rem; margin-top: 2px; text-align: center; }

.score-layout { display: grid; grid-template-columns: auto 1fr; gap: 24px; align-items: start; }
.score-layout .chart-figure { justify-self: center; }
@media (max-width: 640px) { .score-layout { grid-template-columns: 1fr; } .score-layout .chart-figure { margin: 0 auto; } }

.pulse-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin: 12px 0; }
.pulse-chip {
  display: flex; align-items: flex-start; gap: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 12px;
}
.pulse-dot { width: 9px; height: 9px; border-radius: 50%; margin-top: 5px; flex: none; background: var(--status-good); box-shadow: 0 0 0 0 var(--teal-dim); animation: dotpulse 2.2s ease-out infinite; }
.pulse-dot.down { background: var(--status-critical); box-shadow: none; animation: none; }

/* Audit console — the .terminal chrome already matches Tidal's own
   terminal-panel look (traffic-light dots, dark monospace body); this
   just adds the trigger button in the header and a typed-line style for
   the replay script in JS below. */
.audit-console .terminal-header { flex-wrap: wrap; gap: 8px 12px; }
.console-run-btn { cursor: pointer; }
.console-run-btn:disabled { opacity: 0.5; cursor: default; }
.console-output { max-height: 320px; }
.console-line { display: block; margin: 2px 0; }
.console-line .tag-ok { color: var(--status-good); }
.console-line .tag-warn { color: var(--status-warning); }
.console-line .tag-fail { color: var(--status-critical); }
.console-cursor { display: inline-block; width: 7px; height: 1em; background: var(--teal); vertical-align: text-bottom; animation: cursor-blink 1s step-end infinite; }
@keyframes cursor-blink { 50% { opacity: 0; } }
