/* One palette, named by ROLE rather than by shade: the same token has to
   work on both grounds, and a name like --grey-4 says nothing about what
   it should become on white.

   light-dark() keeps a token's two values on ONE line, where they can be
   compared. The alternative -- a second :root block under a media query
   and a third for the manual override -- is these 64 tokens written
   three times, and three copies drift.

   Which half wins is decided by color-scheme: the OS preference by
   default, forced when someone has chosen (see the two rules at the
   bottom of this block). Steps within a family (--panel, --panel-2,
   --panel-3) are how depth is built here without shadows, so they
   survive as separate tokens even where the values sit close together.

   Prominence inverts between the grounds: on dark the brighter shade is
   the louder one, on white the darker. The two ramps are therefore not
   mirror images, and flipping one by hand is how a caption ends up
   shouting. */
:root {
  color-scheme: light dark;

  /* surfaces: the page, then each step up from it */
  --bg:           light-dark(#FAFAFA, #0D1117);
  --sunken:       light-dark(#EBEDF1, #0B0F14);
  --panel:        light-dark(#FFFFFF, #11161D);
  --panel-2:      light-dark(#FCFCFD, #161C24);
  --panel-3:      light-dark(#F1F3F6, #1B222C);
  --panel-4:      light-dark(#ECECEF, #212A36);
  --overlay:      light-dark(#FFFFFF, #0E141B);

  /* hairlines, in the order they get louder */
  --edge:         light-dark(#E4E4E7, #2B313A);
  --edge-2:       light-dark(#D4D4D8, #39434F);
  --card-edge:    light-dark(#E4E4E7, #313A46);
  --edge-3:       light-dark(#C9C9CF, #45505F);
  --edge-4:       light-dark(#B4B4BB, #55606E);

  /* text, brightest first. --stale is "no data" and must never be
     mistakable for --idle, which is a real, measured zero */
  --text:         light-dark(#18181B, #E6EAF0);
  --text-2:       light-dark(#3F3F46, #C2CBD6);
  --text-3:       light-dark(#52525B, #AEB7C2);
  --muted:        light-dark(#5A5A5F, #8D96A1);
  --muted-2:      light-dark(#52525B, #98A2AF);
  --faint:        light-dark(#6B6B72, #7F8A97);
  --faint-2:      light-dark(#71717A, #79818C);
  --faint-3:      light-dark(#63636B, #7E8CA0);
  --dim:          light-dark(#6C6C75, #6D7684);
  --dimmest:      light-dark(#69696F, #5C6774);
  --idle:         light-dark(#71717A, #6B7280);
  --stale:        light-dark(#A1A1AA, #4B5158);

  /* accent blue, which is also the INGRESS lane */
  --accent:       light-dark(#18181B, #6E9BD1);
  --accent-3:     light-dark(#3F3F46, #9FC0E4);
  --accent-tint:  light-dark(#18181B, #C7D9F0);
  --accent-bg:    light-dark(#F4F4F6, #1B2A3D);
  --accent-bg-2:  light-dark(#ECECEF, #22354B);
  --accent-bg-3:  light-dark(#F4F4F6, #17222F);
  --accent-bg-5:  light-dark(#F4F4F6, #22303F);
  --accent-edge:  light-dark(#E4E4E7, #35516F);
  --accent-edge-2:light-dark(#DCDCE0, #2C4260);
  --accent-edge-3:light-dark(#C9C9CF, #4A6E96);
  --accent-edge-4:light-dark(#C9C9CF, #4C5A6B);

  /* green: flowing, and the PERSISTENCE lane */
  --ok:           light-dark(#2C7A58, #5FB88A);
  --ok-2:         light-dark(#337F60, #71AE91);
  --ok-3:         light-dark(#1E6A49, #9FD8B8);
  --ok-bg:        light-dark(#E5F4EC, #101C16);
  --ok-edge:      light-dark(#A9D7C0, #234A38);
  --ok-edge-2:    light-dark(#8FC9AE, #2F5647);

  /* amber: warning, and the DEAD LETTER lane */
  --warn:         light-dark(#9A7400, #E0B400);
  --lane-dl:      light-dark(#A9761A, #D9A441);
  --warn-text:    light-dark(#856508, #E8CE7A);
  --warn-text-2:  light-dark(#7B5D0C, #E8D9A6);
  --warn-text-3:  light-dark(#6A5006, #F3E2A8);
  --warn-text-4:  light-dark(#857024, #D9C08A);
  --warn-bg:      light-dark(#FCF4DB, #241E08);
  --warn-bg-2:    light-dark(#F8F0D6, #201D0E);
  --warn-edge:    light-dark(#E4CE8B, #4A3F10);

  /* red: critical */
  --crit:         light-dark(#BE3A2C, #D64545);
  --crit-text:    light-dark(#B03229, #E98080);
  --crit-text-2:  light-dark(#A62D24, #E8A0A0);
  --crit-text-3:  light-dark(#98271F, #E9C1C1);
  --crit-text-4:  light-dark(#9E2A22, #F0B4B4);
  --crit-text-5:  light-dark(#93261E, #F0BDBD);
  --crit-bg:      light-dark(#FBE8E5, #241016);
  --crit-bg-2:    light-dark(#F8DFDC, #2A151A);
  --crit-bg-3:    light-dark(#F4D3CF, #35191F);
  --crit-bg-4:    light-dark(#FDEFED, #1A1113);
  --crit-edge:    light-dark(#E7ADA7, #5A2626);

  /* The INGRESS lane, no longer the same token as the chrome accent.
     They were one colour by accident -- both were #6E9BD1 -- and the
     moment the chrome stopped being blue the lane had to keep its own
     meaning. */
  --lane-ing:      light-dark(#1F4E85, #6E9BD1);
  --lane-ing-text: light-dark(#2A5F9E, #7FA3C8);

  /* Cluster boundaries. Their own tokens rather than the lane's and the
     OK status's, which is what they borrowed before: on white a tinted
     fill large enough to enclose half the diagram reads as a wash over
     the whole page, and the fix must not drag the meaning of green with
     it. The border and the label carry the cluster now; the fill is
     barely there. */
  --group-dip-bg:    light-dark(#F6F8FC, #101720);
  --group-dip-edge:  light-dark(#C7D9EE, #35516F);
  --group-roco-bg:   light-dark(#F5F9F7, #101C16);
  /* The neutral group -- the broker box inside the DIP cluster. It was
     --panel, i.e. paper white, and being the largest single area on the
     diagram it set the tone for all of it. */
  --group-sys-bg:    light-dark(#FAFBFC, #11161D);
  --group-roco-edge: light-dark(#B6D8C6, #2F5647);

  /* Identity, and the only place it appears: the brand mark and the bar
     on the active nav item. The same value on both grounds, because a
     brand colour that flips with the theme is not one. Everything else
     is neutral, which is what leaves green/amber/red free to mean
     exactly one thing. */
  --ident:         #EFA138;
  --ident-ink:     light-dark(#3B2A0A, #241905);

  /* the REALTIME lane */
  --lane-rt:      light-dark(#6B4CA6, #A98BD1);
}

/* A choice outranks the OS, in both directions. Nothing else keys off
   data-theme -- it flips color-scheme, and light-dark() above does the
   rest, which is why no rule below this line mentions a theme. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

* { box-sizing: border-box; }

/* The `hidden` attribute loses to ANY display declaration, so an element
   with display:flex or display:inline-block stays visible while claiming
   to be hidden. This shipped an empty issue pill under every hop and a
   zoom toolbar on phones. */
[hidden] { display: none !important; }

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

header {
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
  padding: 18px 24px;
  border-bottom: 1px solid var(--edge);
}

header { align-items: center; height: 57px; padding: 0 24px; }
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
header .spacer { margin-left: auto; }

/* Refresh control. The rate is a setting, not a constant: a pipeline
   being watched during an incident and one left open on a wall want
   very different cadences. */
.refresh {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--muted);
  font-size: 12px;
}
.refresh .ico { font-size: 13px; }
.refresh select {
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: var(--panel-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: 4px 6px;
  cursor: pointer;
}
.refresh select:hover { color: var(--text); }

#now {
  font: inherit;
  font-size: 13px;
  line-height: 1;
  padding: 5px 8px;
  color: var(--muted);
  background: var(--panel-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  cursor: pointer;
}
#now:hover { color: var(--text); background: var(--panel-3); }
.refresh select:focus-visible,
#now:focus-visible,
.nav-item:focus-visible,
.profile:focus-visible,
.side-toggle:focus-visible,
.pd-actions button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

#stamp {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  min-width: 148px;
  text-align: right;
}
#stamp.bad { color: var(--warn); }

/* ---- shell: sidebar + content ------------------------------------- */

body:not(.centred) { display: flex; min-height: 100vh; }

.app {
  flex: 1;
  min-width: 0;              /* let the wide diagram scroll, not stretch */
  display: flex;
  flex-direction: column;
}

#side {
  width: 208px;
  flex: 0 0 208px;
  display: flex;
  flex-direction: column;
  background: var(--sunken);
  border-right: 1px solid var(--edge);
  transition: width .14s ease, flex-basis .14s ease;
}
#side.is-collapsed { width: 56px; flex-basis: 56px; }

.side-top {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 16px 12px;
  height: 57px;              /* line up with the top bar's baseline */
  border-bottom: 1px solid var(--edge);
}
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  color: var(--text);
  min-width: 0;
}
.brand-mark {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--ident-ink);
  background: var(--ident);
  border: 1px solid transparent;
  border-radius: 6px;
}
.brand-name { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
.side-toggle {
  margin-left: auto;
  font: inherit;
  font-size: 12px;
  line-height: 1;
  padding: 4px 6px;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 5px;
  cursor: pointer;
}
.side-toggle:hover { color: var(--text); background: var(--panel-2); }
#side.is-collapsed .brand-name,
#side.is-collapsed .nav-label,
#side.is-collapsed .nav-soon,
#side.is-collapsed .profile-text { display: none; }
#side.is-collapsed .side-toggle { display: none; }
#side.is-collapsed .side-top,
#side.is-collapsed .nav-item,
#side.is-collapsed .profile { justify-content: center; padding-left: 0; padding-right: 0; }
/* Collapsed, the brand mark IS the expand control -- the toggle button
   is hidden, so this is the only way back. Wired in sidebar.js.
   `pointer`, not `e-resize`: e-resize means "drag my edge to resize",
   and this is a click. The cursor should describe the interaction, not
   the outcome. */
#side.is-collapsed .brand { cursor: pointer; }
#side.is-collapsed .brand:hover .brand-mark {
  background: var(--accent-bg-2);
  border-color: var(--accent-edge-3);
}

.side-nav { padding: 10px 8px; display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 7px;
  text-decoration: none;
  color: var(--muted);
  font-size: 13px;
  border: 1px solid transparent;
}
.nav-item:hover { color: var(--text); background: var(--panel); }
/* Neutral ground, and the one bar of colour. Hue on this page means
   status -- an accent wash behind a nav item would be the only place it
   meant "you are here", which is the reading a dashboard cannot spare. */
.nav-item.is-on {
  color: var(--accent-tint);
  background: var(--accent-bg-3);
  border-color: var(--accent-edge-2);
  box-shadow: inset 2px 0 0 var(--ident);
}
.nav-ico { width: 16px; height: 16px; flex: 0 0 16px; opacity: 0.85; }
.nav-label { white-space: nowrap; }
.nav-item.is-soon { opacity: 0.45; pointer-events: none; }
.nav-soon {
  margin-left: auto;
  font-size: 9.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--dim);
}

.side-foot {
  margin-top: auto;
  padding: 10px 8px 12px;
  border-top: 1px solid var(--edge);
  position: relative;
}

/* Three states in one control: system, light, dark. It lives in the
   profile dialog with the other two things you can change about the page
   rather than about the data, so it is sized to a dialog row: intrinsic
   width, aligned right against its label like the site select. */
.theme-switch {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: var(--panel-2);
}
.th-opt {
  min-width: 34px;
  padding: 4px 8px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--faint);
  font: inherit;
  font-size: 11px;
  line-height: 1.2;
  cursor: pointer;
}
.th-opt:hover { color: var(--text-2); background: var(--panel-3); }
.th-opt.is-on {
  background: var(--accent-bg);
  color: var(--accent-tint);
  box-shadow: inset 0 0 0 1px var(--accent-edge);
}
.th-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }


.profile {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  font: inherit;
  text-align: left;
  padding: 7px 8px;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 7px;
  cursor: pointer;
  min-width: 0;
}
.profile:hover { background: var(--panel); border-color: var(--edge); }
.avatar {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--accent-3);
  background: var(--accent-bg);
  border: 1px solid var(--accent-edge);
  border-radius: 50%;
}
.profile-text { display: flex; flex-direction: column; min-width: 0; }
.profile-name {
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.profile-role { font-size: 10.5px; color: var(--muted); }

/* Native <dialog>: Esc, backdrop and focus handling come free. */
.profile-dialog {
  width: min(400px, calc(100vw - 40px));
  padding: 22px;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 12px;
  box-shadow: 0 18px 48px rgba(0,0,0,.55);
}
.profile-dialog::backdrop { background: rgba(6,9,13,.62); }

.pd-head {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 18px;
}
.avatar-lg { width: 42px; height: 42px; font-size: 16px; }
.pd-name { margin: 0; font-size: 15px; font-weight: 600; }
/* overflow-wrap, NOT word-break: break-all split the address mid-word
   even when it would have fitted. */
.pd-email {
  margin: 2px 0 0;
  font-size: 12.5px;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.pd-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin: 0 0 18px;
  padding: 10px 12px;
  font-size: 12.5px;
  color: var(--muted);
  background: var(--overlay);
  border: 1px solid var(--edge);
  border-radius: 8px;
}
.pd-row strong { color: var(--text); font-size: 12.5px; }
/* A row whose right-hand side is a control, not a word. */
.pd-theme { align-items: center; }

.pd-actions { display: flex; gap: 10px; }
.pd-actions button {
  flex: 1;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  padding: 9px;
  color: var(--text);
  background: var(--panel-3);
  border: 1px solid var(--edge);
  border-radius: 7px;
  cursor: pointer;
}
.pd-actions button:hover:not(:disabled) { background: var(--panel-4); }
.pd-actions button:disabled { opacity: 0.6; cursor: default; }
.profile-dialog .pd-secondary { flex: 0 0 auto; padding: 9px 16px; color: var(--muted); }
.profile-dialog #signout {
  color: var(--crit-text-3);
  background: var(--crit-bg-2);
  border-color: var(--crit-edge);
}
.profile-dialog #signout:hover:not(:disabled) { background: var(--crit-bg-3); }
.pd-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

#main {
  padding: 24px 24px 48px;
}

.site {
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 10px;
  padding: 18px 18px 14px;
  min-width: 0;
}

.site h2 {
  margin: 0 0 16px;
  font-size: 14px;
  font-weight: 600;
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

.site h2 .slug {
  font-weight: 400;
  font-size: 11.5px;
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* The stage is a fixed viewport; panzoom.js drives a transform on the
   inner <g>. Opens fitted to the whole pipeline, then zoom in for
   detail. No scrollbars, no shrink-to-fit -- the view is a choice now. */
.canvas {
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: var(--sunken);
}
svg.stage { display: block; }

/* Scroll mode (default): the SVG is sized to its natural pixels and this
   wrapper scrolls. Native scrollbar, native trackpad behaviour. */
.canvas.mode-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
}
.canvas.mode-scroll svg.stage { cursor: default; }

/* Drag mode: the SVG is a fixed viewport and panzoom.js moves the
   contents by transform. */
.canvas.mode-drag { overflow: hidden; }
.canvas.mode-drag svg.stage { cursor: grab; touch-action: pan-y; }
.canvas.mode-drag svg.stage.grabbing { cursor: grabbing; }

.canvas.mode-scroll::-webkit-scrollbar { height: 10px; }
.canvas.mode-scroll::-webkit-scrollbar-track {
  background: var(--bg);
  border-radius: 0 0 7px 7px;
}
.canvas.mode-scroll::-webkit-scrollbar-thumb {
  background: var(--edge);
  border-radius: 5px;
}
.canvas.mode-scroll::-webkit-scrollbar-thumb:hover { background: var(--edge-2); }

.lane {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
}

.lane-ING { fill: var(--lane-ing); }
.lane-DL  { fill: var(--lane-dl); }
.lane-PER { fill: var(--ok); }
.lane-RT  { fill: var(--lane-rt); }

.edge { fill: none; stroke-width: 1.5; stroke-opacity: 0.75; }
.lane-stroke-ING { stroke: var(--lane-ing); }
.lane-stroke-DL  { stroke: var(--lane-dl); }
.lane-stroke-PER { stroke: var(--ok); }
.lane-stroke-RT  { stroke: var(--lane-rt); }

.card { fill: var(--panel-2); stroke: var(--edge); stroke-width: 1; }

/* Same image, two modes. Quiet: it is provenance, not status. */
.tag-bg { fill: var(--panel-3); stroke: var(--card-edge); stroke-width: 1; }
.tag-tx {
  fill: var(--faint);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* An edge that leaves the cluster. Dashed because it is a write across
   the vLAN, not an in-cluster hop. */
.edge.is-vlan { stroke-dasharray: 7 5; }

/* System boundary. Quiet enough to sit behind the cards without
   competing, present enough to say "these are one thing". */
.group-bg {
  fill: var(--panel);
  stroke: var(--edge);
  stroke-width: 1;
  stroke-dasharray: 6 5;
}
.group-label {
  fill: var(--dim);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
}

/* Two clusters and, nested inside one of them, a system. Tinted apart so
   "which cluster is this in" is answerable at a glance -- but kept quiet:
   a boundary is context, not status, and it must never compete with the
   warn/crit colours on the cards inside it. */
.group-bg.tone-dip  { stroke: var(--group-dip-edge); fill: var(--group-dip-bg); }
.group-bg.tone-roco { stroke: var(--group-roco-edge); fill: var(--group-roco-bg); }
/* The nested system stays NEUTRAL. Tinting it amber put a warn-coloured
   wash behind the queue cards, and a boundary must never read as a
   status. Neutral-inside-tinted also says "system within cluster". */
.group-bg.tone-sys  { stroke: var(--edge); fill: var(--group-sys-bg); }

.group-label.tone-dip  { fill: var(--lane-ing-text); }
.group-label.tone-roco { fill: var(--ok-2); }
.group-label.tone-sys  { fill: var(--dim); }

.n-title { fill: var(--muted); font-size: 13px; font-weight: 600; }
.n-val   { fill: var(--dim); font-size: 21px; font-weight: 600;
           font-variant-numeric: tabular-nums; }
.n-note  { fill: var(--text-2); font-size: 15px; }
.n-cap   { fill: var(--faint-2); font-size: 10.5px; }

/* ---- info affordance ----------------------------------------------
   The fork explanation used to sit in the middle of the diagram as body
   text. It is now on demand: hover, or Tab to it. */
.info { cursor: help; }
.info-dot {
  fill: var(--panel-3);
  stroke: var(--edge-4);
  stroke-width: 1;
  transition: fill .12s, stroke .12s;
}
.info-i {
  fill: var(--muted-2);
  font-size: 10.5px;
  font-weight: 700;
  font-family: Georgia, "Times New Roman", serif;
  font-style: italic;
  pointer-events: none;
}
.info:hover .info-dot,
.info:focus-visible .info-dot { fill: var(--accent-bg-5); stroke: var(--accent); }
.info:hover .info-i,
.info:focus-visible .info-i { fill: var(--accent-tint); }
.info:focus { outline: none; }
.info:focus-visible .info-dot { stroke-width: 2; }

.tip {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;         /* never let the tip steal its own hover */
  transition: opacity .12s ease, visibility .12s;
}
.info:hover .tip,
.info:focus-visible .tip { opacity: 1; visibility: visible; }
.tip-bg { fill: var(--overlay); stroke: var(--edge-2); stroke-width: 1; }
.tip-tx { fill: var(--text-3); font-size: 11px; }

@media (prefers-reduced-motion: reduce) {
  .tip, .info-dot { transition: none; }
}

/* What a hop is, and why it sits where it does. One element, moved and
   refilled per card by lib/descpanel.js, painted after every node so it
   is never covered -- SVG obeys paint order and has no z-index. */
.desc-panel {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;         /* never let the panel steal its own hover */
  transition: opacity .12s ease, visibility .12s;
}
.desc-panel.is-open { opacity: 1; visibility: visible; }
.dp-bg { fill: var(--overlay); stroke: var(--edge-2); stroke-width: 1; }
.dp-tx { font-size: 11px; }
.dp-body { fill: var(--text-2); }
/* Dimmer than the prose: the neighbour lines are reference, not the
   answer, and reading order should reach the sentence first. */
.dp-rel  { fill: var(--muted); }
.dp-hint { fill: var(--dim); font-style: italic; }

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

/* Focus ring for the one card that is not a link (the producer runs on
   the customer's network, so it has no logs to open) -- without this it
   is focusable but gives no sign of it. */
.node-w { cursor: help; }
.node-w:focus { outline: none; }
.node-w:focus-visible .card { stroke: var(--accent); stroke-width: 2; }

/* Stale must not be mistakable for zero. */
.n-val.stale {
  fill: var(--stale) !important;
  font-style: italic;
}

/* ---- issue states -------------------------------------------------
   An issue is a zero with a live upstream, or a depth over threshold —
   never a plain zero. Encoded in three places at once (border, rail,
   badge) so it reads at a glance and does not rely on colour alone. */
.node.is-warn  .card { stroke: var(--warn); stroke-width: 1.5; }
.node.is-crit  .card { stroke: var(--crit); stroke-width: 1.5; }
.node.is-stale .card { stroke: var(--stale); stroke-dasharray: 5 4; }

.node.is-warn  .rail { fill: var(--warn); }
.node.is-crit  .rail { fill: var(--crit); }
.node.is-stale .rail { fill: var(--stale); }

.badge { pointer-events: none; }
.badge-bg { fill: var(--crit-bg); stroke: var(--crit); stroke-width: 1; }
.badge-tx { fill: var(--crit-text-4); font-size: 10.5px; font-weight: 600; }
.node.is-warn  .badge-bg { fill: var(--warn-bg); stroke: var(--warn); }
.node.is-warn  .badge-tx { fill: var(--warn-text); }
.node.is-stale .badge-bg { fill: var(--panel-2); stroke: var(--stale); }
.node.is-stale .badge-tx { fill: var(--muted); }

.node.is-crit .card { animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse {
  0%, 100% { stroke-opacity: 1; }
  50%      { stroke-opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
  .node.is-crit .card { animation: none; }
}

/* ---- header controls ---------------------------------------------- */
.site h2 .status {
  font-size: 11px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid;
}
.site h2 .status.ok   { color: var(--ok); border-color: var(--ok-edge); background: var(--ok-bg); }
.site h2 .status.warn { color: var(--warn); border-color: var(--warn-edge); background: var(--warn-bg-2); }
.site h2 .status.crit { color: var(--crit-text); border-color: var(--crit-edge); background: var(--crit-bg); }

.site h2 .tools {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
}
.site h2 .tools button {
  font: inherit;
  font-size: 11.5px;
  line-height: 1;
  padding: 5px 9px;
  color: var(--muted);
  background: var(--panel-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  cursor: pointer;
}
.site h2 .tools button:hover { color: var(--text); background: var(--panel-3); }
.site h2 .tools button.mode.is-on {
  color: var(--accent-tint);
  background: var(--accent-bg);
  border-color: var(--accent-edge);
}
.site h2 .tools button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.site h2 .zoom {
  font-size: 11px;
  color: var(--muted);
  min-width: 40px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}


/* ---- /pipeline/<site> --------------------------------------------- */

.crumb { margin: 0 0 16px; font-size: 12.5px; }
.crumb a { color: var(--muted); text-decoration: none; }
.crumb a:hover { color: var(--text); }

/* ---- not found ----------------------------------------------------- */

.notfound { max-width: 60ch; }
.notfound h2 { font-size: 15px; margin: 0 0 10px; }
.notfound p { color: var(--muted); font-size: 13px; }
.notfound code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text);
}
.notfound ul { list-style: none; padding: 0; }
.notfound li { margin: 6px 0; }
.notfound a { color: var(--accent); }

/* ---- sign-in / no-access ------------------------------------------- */

body.centred {
  display: grid;
  place-items: center;
  min-height: 100vh;
}

.auth {
  width: min(360px, calc(100vw - 48px));
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 12px;
  padding: 28px 26px 22px;
}
.auth h1 { margin: 0 0 6px; font-size: 17px; font-weight: 600; }
.auth-sub { margin: 0 0 20px; color: var(--muted); font-size: 13px; }
.auth-note {
  margin: 16px 0 0;
  color: var(--dim);
  font-size: 11.5px;
}
.auth label {
  display: block;
  margin-bottom: 14px;
  font-size: 12px;
  color: var(--muted);
}
.auth input {
  display: block;
  width: 100%;
  margin-top: 6px;
  font: inherit;
  font-size: 13.5px;
  color: var(--text);
  background: var(--overlay);
  border: 1px solid var(--edge);
  border-radius: 7px;
  padding: 9px 11px;
}
.auth input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent-edge);
}
.auth button {
  width: 100%;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent-tint);
  background: var(--accent-bg);
  border: 1px solid var(--accent-edge);
  border-radius: 7px;
  padding: 10px;
  cursor: pointer;
}
.auth button:hover:not(:disabled) { background: var(--accent-bg-2); }
.auth button:disabled { opacity: 0.6; cursor: default; }
.auth button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.auth-error {
  margin: 0 0 14px;
  padding: 9px 11px;
  font-size: 12.5px;
  color: var(--crit-text);
  background: var(--crit-bg);
  border: 1px solid var(--crit-edge);
  border-radius: 7px;
}


/* ==== hop list: the pipeline where the diagram will not fit ========= */

.hoplist { display: flex; flex-direction: column; gap: 20px; }
.hl-lane-name {
  margin: 0 0 8px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.09em;
}
.hl-hops { list-style: none; margin: 0; padding: 0; }
.hl-hop {
  display: flex;
  gap: 11px;
  padding: 11px 12px;
  border: 1px solid var(--edge);
  border-radius: 9px;
  background: var(--panel);
}
.hl-hop + .hl-hop { margin-top: 8px; }
.hl-hop.is-warn  { border-color: var(--warn-edge); }
.hl-hop.is-crit  { border-color: var(--crit-edge); }
.hl-hop.is-stale { border-style: dashed; }
.hl-dot {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--dim);
}
.hl-body { min-width: 0; flex: 1; }
.hl-title { margin: 0; font-size: 12.5px; font-weight: 600; color: var(--muted); }
.hl-value {
  margin: 3px 0 0;
  font-size: 19px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.hl-value.stale { color: var(--stale); font-style: italic; }
.hl-cap { margin: 4px 0 0; font-size: 10.5px; color: var(--faint-2); }
.hl-issue {
  display: inline-block;
  margin: 7px 0 0;
  padding: 2px 8px;
  font-size: 10.5px;
  font-weight: 600;
  border-radius: 999px;
  color: var(--crit-text-4);
  background: var(--crit-bg);
  border: 1px solid var(--crit);
}
.hl-hop.is-warn  .hl-issue { color: var(--warn-text); background: var(--warn-bg); border-color: var(--warn); }
.hl-hop.is-stale .hl-issue { color: var(--muted); background: var(--panel-2); border-color: var(--stale); }

/* ==== below 1024: the sidebar becomes a drawer ====================== */

.hamburger { display: none; }

@media (max-width: 1023px) {
  .hamburger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font: inherit;
    font-size: 17px;
    line-height: 1;
    width: 34px;
    height: 30px;
    margin-right: 4px;
    color: var(--muted);
    background: var(--panel-2);
    border: 1px solid var(--edge);
    border-radius: 7px;
    cursor: pointer;
  }
  .hamburger:hover { color: var(--text); }

  #side {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 40;
    transform: translateX(-100%);
    transition: transform .18s ease;
  }
  /* Collapsing a drawer is meaningless, and a 56px drawer with hidden
     labels would be the same trap 0.4.1 fixed. */
  #side.is-collapsed { width: 208px; flex-basis: 208px; }
  #side.is-collapsed .brand-name,
  #side.is-collapsed .nav-label,
  #side.is-collapsed .nav-soon,
  #side.is-collapsed .profile-text { display: revert; }
  #side.is-collapsed .side-top,
  #side.is-collapsed .nav-item,
  #side.is-collapsed .profile { justify-content: flex-start; }
  .side-toggle { display: none; }

  #side.is-open { transform: translateX(0); }

  .scrim {
    position: fixed;
    inset: 0;
    z-index: 30;
    background: rgba(6,9,13,.6);
  }
  .scrim[hidden] { display: none; }
}

/* ==== below 640: phone ============================================== */

@media (max-width: 639px) {
  header { padding: 0 14px; gap: 8px; }
  header h1 {
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
  /* The word "updated" is the first thing to go; the time is the point. */
  #stamp { min-width: 0; font-size: 11px; }
  .refresh .ico { display: none; }

  #main { padding: 16px 14px 40px; }
  .site { padding: 14px 14px 12px; }
  .site h2 { gap: 8px; }
  .hl-value { font-size: 17px; }
}

/* ===================================================================
   LOGS
   =================================================================== */

/* A diagram card is a link to its own logs. The affordance has to be
   quiet: the diagram's job is to show state, and a card that shouted
   "clickable" would compete with the issue badges that actually need
   attention. */
.node-a { cursor: pointer; }
.node-a .card { transition: stroke 120ms ease, fill 120ms ease; }
.node-a:hover .card { fill: var(--panel-3); stroke: var(--edge-3); }
.node-a:focus-visible { outline: none; }
.node-a:focus-visible .card { stroke: var(--accent); stroke-width: 2; }

/* On a phone the diagram is not rendered at all, so the hop row IS the
   tap target -- the whole body, not a link buried inside it. */
/* The phone form prints what the diagram hides behind a dwell. */
.hl-desc {
  margin: 6px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-3);
}
.hl-rel {
  margin: 4px 0 0;
  font-size: 11px;
  color: var(--faint);
}

.hl-link {
  flex: 1;
  min-width: 0;
  display: block;
  color: inherit;
  text-decoration: none;
}
.hl-hop.has-logs { cursor: pointer; }
.hl-hop.has-logs:hover { border-color: var(--edge-3); background: var(--panel-2); }
.hl-link:focus-visible { outline: none; }
.hl-hop:focus-within { border-color: var(--accent); }

/* --- the picker --------------------------------------------------- */

.logs { max-width: 1200px; }
.logs h2 { margin: 0 0 6px; font-size: 17px; }
.lg-lead { margin: 0 0 22px; color: var(--muted); font-size: 12.5px; max-width: 62ch; }

.lg-site {
  border: 1px solid var(--edge);
  border-radius: 11px;
  background: var(--panel);
  padding: 16px 18px 18px;
  margin-bottom: 16px;
}
.lg-site h3 { margin: 0 0 14px; font-size: 13.5px; font-weight: 600; }
.lg-lanes { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 18px; }
.lg-lane h4 {
  margin: 0 0 8px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.09em;
}
.lg-lane ul, .lg-joblist { list-style: none; margin: 0; padding: 0; }
.lg-joblist { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 6px; }
.lg-job { font-size: 12.5px; padding: 4px 0; display: flex; align-items: center; gap: 7px; }
.lg-job a { color: var(--text); text-decoration: none; border-bottom: 1px solid transparent; }
.lg-job a:hover { border-bottom-color: var(--accent); color: var(--accent-tint); }
/* A hop with no log source is listed anyway. Omitting it would quietly
   redraw the pipeline as if it had one fewer step. */
.lg-job.is-off { color: var(--stale); font-style: italic; }
.lg-shared {
  font-size: 9.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--edge);
  border-radius: 4px;
  padding: 1px 4px;
}
.lg-jobs { margin-top: 12px; }
.lg-empty, .lg-err { font-size: 12.5px; color: var(--muted); margin: 0; }
.lg-err { color: var(--crit-text-2); }

/* --- the pane ------------------------------------------------------ */

.logs.is-pane { max-width: none; }
.lg-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.lg-where { font-size: 11px; color: var(--muted); letter-spacing: 0.05em; }
.lg-job-name { font-size: 17px; font-weight: 600; }
.lg-back { margin-left: auto; font-size: 12px; color: var(--muted); text-decoration: none; }
.lg-back:hover { color: var(--text); }

.logpane {
  margin-top: 14px;
  border: 1px solid var(--edge);
  border-radius: 11px;
  background: var(--panel);
  overflow: hidden;
}
.lp-bar {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-bottom: 1px solid var(--edge);
}
.lp-field { display: flex; flex-direction: column; gap: 4px; }
.lp-field > span {
  font-size: 9.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
.lp-field select, .lp-field input {
  font: inherit;
  font-size: 12px;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: 5px 7px;
}
.lp-field select { cursor: pointer; }
.lp-grow { flex: 1; min-width: 180px; }
.lp-grow input { width: 100%; }
.lp-actions { display: flex; gap: 6px; margin-left: auto; }
.lp-btn {
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: var(--panel-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: 6px 11px;
  cursor: pointer;
}
.lp-btn:hover { color: var(--text); border-color: var(--edge-3); }
.lp-btn.is-on { color: var(--ok-3); border-color: var(--ok); background: var(--ok-bg); }

.lp-status {
  padding: 7px 14px;
  font-size: 11.5px;
  color: var(--muted);
  border-bottom: 1px solid var(--edge);
  background: var(--sunken);
}
/* Empty and unreachable are DIFFERENT states and are coloured
   differently on purpose. An unreachable Loki rendered as a quiet job is
   the exact misreading that hid ROCO's dead log pipeline for 196 days. */
.lp-status.is-empty { color: var(--text-3); }
.lp-status.is-error { color: var(--crit-text-2); background: var(--crit-bg-4); }

.lp-body {
  height: min(62vh, 720px);
  overflow: auto;
  background: var(--sunken);
  font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  padding: 8px 0;
}
.lp-body:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
.lp-line {
  display: flex;
  gap: 10px;
  padding: 1px 14px 1px 11px;
  border-left: 3px solid transparent;
  white-space: pre;
}
.lp-line:hover { background: var(--panel); }
/* Long JSON is the norm here, so the default does NOT wrap: a wall of
   reflowed text is harder to scan than one that scrolls. Wrap is one
   click away for when a single line is the thing you are reading. */
.lp-body.is-wrap .lp-line { white-space: pre-wrap; }
.lp-body.is-wrap .lp-msg { word-break: break-word; }
.lp-ts { color: var(--dimmest); flex: 0 0 auto; }
.lp-src { color: var(--faint-3); flex: 0 0 auto; }
.lp-msg { flex: 1; min-width: 0; color: var(--text-2); }
.lp-line.lv-error, .lp-line.lv-critical, .lp-line.lv-fatal {
  border-left-color: var(--crit);
}
.lp-line.lv-error .lp-msg, .lp-line.lv-critical .lp-msg,
.lp-line.lv-fatal .lp-msg { color: var(--crit-text-5); }
.lp-line.lv-warn { border-left-color: var(--warn); }
.lp-line.lv-warn .lp-msg { color: var(--warn-text-2); }
.lp-line.lv-debug .lp-msg { color: var(--muted); }

@media (max-width: 639px) {
  .lg-lanes { grid-template-columns: 1fr; gap: 14px; }
  .lg-site { padding: 14px; }
  .lp-bar { gap: 9px; padding: 11px; }
  .lp-actions { margin-left: 0; width: 100%; }
  .lp-btn { flex: 1; padding: 8px 6px; }
  .lp-grow { min-width: 100%; }
  /* Nothing else on the page competes for height on a phone, and a
     short log pane is useless. */
  .lp-body { height: 64vh; font-size: 11px; }
  .lp-line { padding-right: 10px; }
  .lg-back { margin-left: 0; }
}

/* --- the active site ---------------------------------------------- */

/* A button that does not look like one until you reach for it: the site
   name is the rail's headline first and the way to change it second, so
   the affordance is a hover, not a permanent chrome box. */
.side-site {
  width: 100%;
  padding: 10px 14px 12px;
  border: 0;
  border-bottom: 1px solid var(--edge);
  border-radius: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.side-site:hover { background: var(--panel-3); }
.side-site:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ss-k {
  font-size: 9px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
.ss-v {
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Collapsed to the rail there is no room for a name, and a truncated one
   reads as a bug. The dot says a site is selected; the profile dialog
   says which. */
#side.is-collapsed .side-site { align-items: center; padding: 10px 0 12px; }
#side.is-collapsed .ss-k, #side.is-collapsed .ss-v { display: none; }
#side.is-collapsed .side-site::after {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

.pd-site { align-items: center; }
.pd-site select {
  font: inherit;
  font-size: 12.5px;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: 5px 7px;
  cursor: pointer;
}

/* A shared job has no site of its own. This is a correction to what the
   address bar implies, so it is styled as a caution rather than a note. */
.lg-warn {
  margin: 12px 0 0;
  padding: 8px 11px;
  font-size: 12px;
  color: var(--warn-text);
  background: var(--warn-bg);
  border: 1px solid var(--warn);
  border-radius: 7px;
}
.lg-warn strong { color: var(--warn-text-3); }

/* What this hop is, carried over from the diagram. Sits between the job
   name and the pane: read on the way in, then scrolled past. Quiet on
   purpose -- it explains the page, it is not a finding, and it must not
   compete with the .lg-warn that can sit directly above it. */
.lg-desc {
  margin: 12px 0 0;
  padding: 11px 13px;
  border: 1px solid var(--edge);
  border-left: 2px solid var(--accent-edge-4);
  border-radius: 7px;
  background: var(--panel);
  max-width: 78ch;
}
.lg-desc h3 { margin: 0 0 5px; font-size: 12px; font-weight: 600; }
.lg-desc p { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--text-2); }
/* Two classes, so it outranks `.lg-desc p` without !important. */
.lg-desc .lg-desc-rel {
  margin-top: 6px;
  font-size: 11px;
  color: var(--muted);
}
.lg-note { margin: 0 0 12px; font-size: 11.5px; color: var(--muted); }

/* A date rule between days. Sticky, so scrolling through a 7-day range
   never leaves you without the date of what is on screen. */
.lp-day {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px 6px 11px;
  background: linear-gradient(var(--sunken) 70%, rgba(11, 15, 20, 0));
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.lp-day span { flex: 0 0 auto; }
.lp-day::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--edge);
}
/* A full result means the limit was hit, not that this is all there is.
   Distinct from empty and from error: it is a caveat, not a failure. */
.lp-status.is-capped { color: var(--warn-text-4); }
