@import url('/theme.css');

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

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--fg);
  font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font: inherit; }

/* ------------------------------------------------------------- layout --- */

#app { display: flex; min-height: 100vh; }

.sidebar {
  width: 236px;
  flex: 0 0 236px;
  background: var(--panel);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
}

.brand {
  padding: 16px 18px;
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.02em;
  display: flex;
  align-items: center;
  gap: 9px;
  border-bottom: 1px solid var(--line);
}
.brand .dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--good);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 22%, transparent);
}

/*
 * The scope control sits in the topbar, beside the range picker: what, then
 * when. It is the widest control up there because an application name is the
 * one thing in the bar that is not a fixed vocabulary.
 */
.topbar select.scope {
  width: auto; max-width: 260px; padding: 6px 10px; font-size: 13px;
  background: var(--panel-2); border-color: var(--line-2);
}
@media (max-width: 800px) { .topbar select.scope { max-width: 44vw; } }
.nav { padding: 10px 8px; flex: 1; overflow-y: auto; }
.nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 11px; margin-bottom: 2px;
  border-radius: 7px; color: var(--fg-2); font-weight: 500;
}
.nav a:hover { background: var(--panel-2); color: var(--fg); text-decoration: none; }
.nav a.active {
  background: var(--accent-bg);
  color: var(--accent);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--accent);
}
.nav .sec {
  padding: 14px 11px 6px; font-size: 11px; text-transform: uppercase;
  letter-spacing: .08em; color: var(--fg-3); font-weight: 600;
}
.nav .badge { margin-left: auto; }

.sidebar-foot { padding: 12px; border-top: 1px solid var(--line); font-size: 12px; color: var(--fg-3); }

main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 20px;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(8px);
  position: sticky; top: 0; z-index: 20;
}
.topbar h1 { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -.01em; }
.topbar .spacer { flex: 1; }

.content { padding: 20px; width: 100%; }
/* Running text keeps a measure even when the cards do not. */
.content p, .glossary dd, .vuln > span:nth-child(3) { max-width: 90ch; }

/* ------------------------------------------------------------ elements --- */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: var(--shadow);
}
.card h3 {
  margin: 0 0 12px; font-size: 13px; font-weight: 650; color: var(--fg-2);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.card h3 .spacer { flex: 1; }

/* A heading for a GROUP of cards. The same type as a card's own h3, without a
   box: a card wrapped around cards is one border too many, and the group still
   needs the sentence that says what its cards are. */
.secthead {
  margin: 14px 0 10px; font-size: 13px; font-weight: 650; color: var(--fg-2);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.secthead .drill { text-transform: none; letter-spacing: 0; }

/*
 * The path from a visit to the backend, at the top of an overview.
 *
 * It was a row of grey rectangles with arrow characters wedged between the
 * columns: a list with punctuation, which is what it looked like. A flow is
 * readable because of its CONNECTIONS — a curve from the box a call left to
 * the box that answered it, thicker where more calls went through — so it is
 * drawn as SVG (see flowPath in charts.js) and this is only its palette.
 *
 * Colour carries the stage, not the state: one hue per column, with warn and
 * bad kept for a box that is actually failing, so a coloured edge on this
 * picture means what it means everywhere else on the page.
 */
/*
 * The picture keeps its own size and the box scrolls.
 *
 * `.card svg { max-width: 100% }` squeezed it on a phone: the width shrank to
 * the card, the height attribute did not, and `meet` scaling then drew the
 * whole thing at a third of its size in the middle of a frame twice as tall as
 * the content — a letterbox. A path is read by scrolling sideways; it is not
 * read at all when the type is four pixels high.
 */
.flowbox { overflow-x: auto; padding-bottom: 2px; }
.flowbox .flow-svg { max-width: none; }
/* Its own class, because `.flow` is already a column layout on this page. */
.flow-svg { display: block; }
.flow-cap { fill: var(--fg-3); font-size: 9px; letter-spacing: .07em; font-weight: 650; }
.flow-more { fill: var(--fg-3); font-size: 10.5px; }
.flow-edge { fill: none; stroke: var(--line-2); opacity: .9; }
.flow-edge.dim { stroke-dasharray: 3 4; opacity: .5; }
.flow-box {
  fill: var(--panel-2); stroke: var(--line); stroke-width: 1;
  transition: stroke .12s ease, fill .12s ease;
}
.flow-node { cursor: pointer; }
.flow-node:hover .flow-box { stroke: var(--accent); fill: var(--panel); }
/* The box you are standing on: every entity page draws this same picture. */
.flow-box.is-current { stroke: var(--accent); stroke-width: 1.6; fill: var(--accent-bg); }

/* Zoom: the picture is SVG, so this only changes how big it is shown. */
.flowwrap { position: relative; }
/* The fold box: a control, not a measurement — so it is drawn as one. */
.flow-box.is-fold { fill: transparent; stroke-dasharray: 4 3; }
.flow-node.is-fold { cursor: pointer; }
.flow-node.is-fold:hover .flow-box { stroke: var(--accent); fill: var(--panel); }
.flow-box.h-warn { stroke: var(--warn); }
.flow-box.h-bad { stroke: var(--bad); }
/* The type grew with the box: a taller box with the old two lines in it just
   has more empty space at the bottom. */
.flow-label { fill: var(--fg); font-size: 12.5px; font-weight: 600; }
.flow-sub { fill: var(--fg-3); font-size: 11px; }
.flow-dot { fill: var(--fg-3); }
.flow-dot.t-visit { fill: var(--c-app); }
.flow-dot.t-own { fill: var(--c-domain); }
.flow-dot.t-third { fill: var(--fg-3); }
.flow-dot.t-service { fill: var(--c-service); }
.flow-dot.t-data { fill: var(--s3); }
.flow-dot.t-host { fill: var(--c-host); }

/*
 * What is worth instrumenting next: one row per piece of advice, three
 * columns — what and why, what it would show, and the way to do it. The
 * middle column is the reason the row exists, so it is not a tooltip.
 */
/*
 * A sampled request, opened. Two panes side by side because the question is
 * almost always what went in against what came back; on a phone they stack,
 * which is the same question read one after the other.
 */
/*
 * A captured body, read rather than dumped.
 *
 * The tree this replaces drew every element of every array with a path column
 * down the right-hand side; on a phone that was forty rows of framework
 * internals with three useful values inside them, and the value column was
 * squeezed to nothing by the path. Here the key IS the path, the value gets
 * the room, and anything bigger than a value is one line until it is asked
 * for.
 */
/*
 * The vocabulary, as a globe.
 *
 * Every field the captured bodies carry, placed on a sphere: the common ones
 * large and near, the rare ones small and far. It turns by itself until
 * somebody touches it and then stays where they left it — a picture that
 * keeps moving under a reader trying to read one label is a toy.
 *
 * `touch-action: none` because the gesture is the picture's: one finger turns
 * it, two zoom it, and the page must not take the drag away halfway through.
 */
.cloud {
  position: relative;
  height: clamp(320px, 46vh, 560px);
  border-radius: 12px;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  /*
   * Black, in both themes.
   *
   * The globe is a night sky with words in it: on the light theme's panel the
   * far side of the sphere had to be dimmed into invisibility to read as far
   * away, and the near side had nothing to be bright against. On black the
   * depth does the work and the two colours below carry their whole meaning.
   */
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--void-res) 12%, transparent), transparent 62%),
    var(--void);
  border: 1px solid var(--void-line);
}
.cloud:active { cursor: grabbing; }
.cloud:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cloudsvg { display: block; width: 100%; height: 100%; }
.cloudlabel {
  font-family: var(--mono);
  fill: var(--void-both);
  cursor: pointer;
  paint-order: stroke;
  stroke: var(--void);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.cloudlabel:hover { fill: var(--void-hi); }
/*
 * The colour says which way the field travels — green out, blue back — and
 * not what type it is: the type is readable from the value printed beside it,
 * the direction is readable from nothing else.
 */
.cloudlabel.o-req { fill: var(--void-req); }
.cloudlabel.o-res { fill: var(--void-res); }
.cloudlabel.o-both { fill: var(--void-both); }
/*
 * The filter, where it can be seen.
 *
 * It started as three small pills in the bottom corner doubling as the legend
 * — and on a globe with two hundred labels on it, the labels went straight
 * over them: the person who asked for the filter could not find it. So it is
 * a bar across the top on its own ground, and the legend it also is comes
 * along with it.
 */
.cloudbar {
  position: absolute; left: 10px; right: 10px; top: 10px; z-index: 2;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 6px; border-radius: 10px;
  background: color-mix(in srgb, var(--void) 72%, transparent);
  border: 1px solid var(--void-line);
  backdrop-filter: blur(6px);
}
.ckey {
  font: inherit; font-size: 12px; cursor: pointer;
  padding: 4px 10px; border-radius: 999px;
  background: color-mix(in srgb, var(--void-line) 60%, transparent);
  border: 1px solid var(--void-line); color: var(--void-both);
}
.ckey::before { content: '●'; margin-right: 5px; }
.ckey:hover { border-color: currentColor; }
.ckey.on { background: color-mix(in srgb, currentColor 20%, transparent); border-color: currentColor; }
.ckey.o-req { color: var(--void-req); }
.ckey.o-res { color: var(--void-res); }
.ckey.o-both { color: var(--void-both); }
.cloudtip {
  position: absolute; right: 12px; bottom: 10px; pointer-events: none;
  color: var(--void-both); opacity: .65;
}
@media (max-width: 620px) { .cloudtip { display: none; } }
/* What was clicked, and the way from a name to the rows that carry it. It is
   at the bottom because the filter now owns the top. */
.cloudpick {
  position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 2;
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px;
  backdrop-filter: blur(6px);
}
.pickrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pickval { color: var(--fg); font-size: 12px; }
@media (max-width: 620px) {
  .cloud { height: clamp(260px, 52vh, 420px); }
  .cloudbar { gap: 4px; padding: 5px; }
}

.bpane { min-width: 0; }
.bhead { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.bhead .k {
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3);
}
.bview {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 4px 2px; max-height: 340px; overflow: auto;
}
.bview > pre { margin: 0; padding: 6px 8px; white-space: pre-wrap; word-break: break-word; }
.braws {
  margin: 0; padding: 8px 10px; max-height: 340px; overflow: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  white-space: pre-wrap; word-break: break-word;
}
.bfield {
  display: grid; grid-template-columns: minmax(6ch, 22ch) minmax(0, 1fr);
  gap: 4px 10px; align-items: baseline; padding: 2px 8px; border-radius: 4px;
}
.bfield:hover { background: var(--panel); }
.bk { color: var(--fg-3); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bv {
  font-size: 12px; color: var(--fg); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bv.t-string { color: var(--s3); }
.bv.t-number { color: var(--s1); }
.bv.t-boolean, .bv.t-null { color: var(--s2); }
/* One line for a structure: what it is, and how much of it there is. */
.bgroup > .bopen {
  display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 3px 8px;
  font: inherit; text-align: left; cursor: pointer; border-radius: 4px;
  background: transparent; border: 0; color: var(--fg-2);
}
.bgroup > .bopen:hover { background: var(--panel); }
.bcar { flex: none; width: 9px; color: var(--fg-3); font-size: 10px; }
.bcar::before { content: '▸'; }
.bgroup.open > .bopen .bcar::before { content: '▾'; }
.bnoun { color: var(--fg-3); font-size: 11px; }
.bkids { margin-left: 12px; border-left: 1px solid var(--line); padding-left: 6px; }
.bblank { padding: 2px 8px; }
.blink {
  font: inherit; font-size: 11px; color: var(--fg-3); background: transparent; border: 0;
  padding: 2px 0; cursor: pointer; text-decoration: underline dotted;
}
.blink:hover { color: var(--fg-2); }
/*
 * On a phone the key goes above its value: 22 characters of path and a value
 * beside it in 360px means neither can be read.
 */
@media (max-width: 620px) {
  .bfield { grid-template-columns: minmax(0, 1fr); gap: 0; padding: 4px 8px; }
  .bfield + .bfield { border-top: 1px solid var(--line); }
  .bv { white-space: normal; word-break: break-word; }
}

.jfilter { min-width: 180px; }

/*
 * One captured request in the store: a line to scan, and the body it carried
 * one click below it. Opened in place, because moving to another page to read
 * one body loses the list somebody built with four filters.
 */
/*
 * The workflow editor: a palette, a canvas, and the box that is selected.
 *
 * Three columns rather than a floating panel, because the thing being drawn is
 * wide and the thing being filled in is tall, and a panel that covers the
 * canvas hides the connection somebody is about to make.
 */
/*
 * The workflow page: the list beside the canvas, not above it.
 *
 * A table of workflows across the top pushed the drawing below the fold, so
 * opening one began with a scroll — and the two things somebody does here,
 * "open that one" and "switch this one off", both belong next to the thing
 * they are about.
 */
/*
 * The page is the editor, so it takes the screen.
 *
 * The canvas had a 380px floor and both cards were laid out from the top, so
 * on a wide window the drawing stopped a third of the way down and the rest
 * was empty ground — the boxes crowded into a strip while the room they
 * wanted sat unused underneath. Both columns now run to the bottom of the
 * window, and the canvas takes whatever the header and the hint leave.
 */
.wfpage {
  display: grid; grid-template-columns: 240px 1fr; gap: 14px;
  align-items: stretch;
  /*
   * The window, exactly — not "at least".
   *
   * A minimum let the tallest column decide: the panel on the right carries a
   * trigger's whole form plus the names the workflow hands on, and that made
   * the card taller than the screen, so the whole page scrolled and the
   * drawing went with it. The page is the height of the window and the two
   * things that can overflow — the panel and the drawing — scroll inside
   * themselves, where scrolling costs nothing.
   *
   * 61px topbar + the content padding above and below it, measured by
   * test:wffill rather than assumed.
   */
  height: calc(100vh - 104px);
}
.wflist, .wfcard { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.wfcard > .wfwrap { flex: 1; min-height: 0; }
.wflist > div, .wfpal, .wfside { overflow-y: auto; min-height: 0; }
/*
 * The drawing and the one button that belongs to it.
 *
 * Save sits in the canvas's bottom-right corner rather than in the card
 * header: it is the act that finishes a drawing, so it belongs where the hand
 * already is. It floats over the canvas instead of scrolling with it, because
 * a Save that has scrolled out of sight is a Save nobody presses.
 */
.wfstage { position: relative; min-height: 0; display: flex; }
.wfstage > .wfcanvas { flex: 1; }
.wfsave { position: absolute; right: 12px; bottom: 12px; z-index: 3; box-shadow: var(--shadow); }

.wfcanvas { height: 100%; }
/*
 * On a phone the columns are stacked, and a fixed height would mean three
 * scrolling boxes inside one scrolling page.
 */
@media (max-width: 1100px) {
  .wfpage { grid-template-columns: 1fr; height: auto; }
  .wflist, .wfcard { overflow: visible; }
  .wflist > div, .wfpal, .wfside { overflow-y: visible; }
  .wfcanvas { height: 420px; }
}
.wflist { padding: 12px; }
.wflist input[type=search] { width: 100%; margin-bottom: 8px; }
.wfrows { display: flex; flex-direction: column; }
.wfrow {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px;
  padding: 7px 4px; border-top: 1px solid var(--line);
}
.wfrow:first-child { border-top: 0; }
.wfrow.sel { background: var(--accent-bg); border-radius: 8px; }
.wfrow[hidden] { display: none; }
.wfrname { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-decoration: none; }
.wfrname b { color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wfrname .small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wfrdel {
  font: inherit; background: transparent; border: 0; color: var(--fg-3); cursor: pointer;
  padding: 2px 4px; border-radius: 6px;
}
.wfrdel:hover { color: var(--bad); background: var(--panel-2); }

.wfcard .wfname {
  font: inherit; font-size: 13px; font-weight: 650; color: var(--fg);
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  padding: 2px 6px; min-width: 200px;
}
.wfcard .wfname:hover, .wfcard .wfname:focus { border-color: var(--line); background: var(--panel-2); }
.wfwrap { display: grid; grid-template-columns: 170px 1fr 230px; gap: 12px; align-items: stretch; }
@media (max-width: 1100px) { .wfwrap { grid-template-columns: 1fr; } }

.wfpal { display: flex; flex-direction: column; gap: 10px; }
.wfgroup { display: flex; flex-direction: column; gap: 4px; }
.wfgroup > .k {
  font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-3);
}
.wfadd {
  font: inherit; font-size: 11.5px; text-align: left; cursor: pointer;
  padding: 5px 8px; border-radius: 8px; color: var(--fg-2);
  background: var(--panel-2); border: 1px solid var(--line);
}
.wfadd:hover { border-color: var(--accent); color: var(--fg); }
.wfadd.is-trigger { border-left: 3px solid var(--s1); }
.wfadd.is-step { border-left: 3px solid var(--s3); }
.wfadd.is-action { border-left: 3px solid var(--s2); }

.wfcanvas {
  position: relative; min-height: 380px; overflow: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
  background-image: radial-gradient(var(--line) 1px, transparent 1px);
  background-size: 18px 18px;
}
.wfcanvas.linking { cursor: crosshair; }
.wfedges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.wfedge { fill: none; stroke: var(--line-2); stroke-width: 2; pointer-events: stroke; cursor: pointer; }
.wfedge:hover { stroke: var(--bad); }

.wfnode {
  position: absolute; width: 190px; min-height: 74px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: var(--shadow); font-size: 11.5px;
}
.wfnode.sel { border-color: var(--accent); }
.wfnode.is-trigger { border-left: 3px solid var(--s1); }
.wfnode.is-step { border-left: 3px solid var(--s3); }
.wfnode.is-action { border-left: 3px solid var(--s2); }
.wfhead {
  display: flex; align-items: center; gap: 6px; padding: 6px 8px; cursor: grab;
  font-weight: 600; color: var(--fg-2); border-bottom: 1px solid var(--line);
}
.wfhead .wfx { margin-left: auto; color: var(--fg-3); cursor: pointer; }
.wfhead .wfx:hover { color: var(--bad); }
.wfbody { padding: 6px 8px; color: var(--fg-3); word-break: break-word; }
.wfwarn { padding: 0 8px 6px; color: var(--warn); font-size: 10.5px; }
/* The two dots that make a line: out on the right, in on the left. */
.wfout, .wfin {
  position: absolute; top: 30px; width: 11px; height: 11px; border-radius: 50%;
  background: var(--panel-2); border: 1px solid var(--line-2);
}
.wfout { right: -6px; cursor: crosshair; }
.wfout:hover { border-color: var(--accent); background: var(--accent); }
.wfin { left: -6px; }

.wfside { display: flex; flex-direction: column; gap: 8px; }
/*
 * The run strip, under the drawing: what this workflow has actually done.
 */
.wfruns { margin-top: 10px; }
.wfrunhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }

/*
 * The names a workflow carries. A chip per key, because the list is read by
 * eye and clicked by hand — and it is monospace because these are identifiers
 * that get typed into a body somewhere else, where one wrong character is a
 * silently empty value.
 */
.wfctx { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 8px; }
.wfctx > .k { font-size: 10.5px; color: var(--fg-3); }
.wfchips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.wfchip {
  font: inherit; font-family: var(--mono); font-size: 10.5px; cursor: pointer;
  padding: 2px 6px; border-radius: 6px; color: var(--fg-2);
  background: var(--panel-2); border: 1px solid var(--line);
}
.wfchip:hover { border-color: var(--accent); color: var(--fg); }
.wfabout { line-height: 1.35; }

.wffield { display: flex; flex-direction: column; gap: 3px; }
.wffield > .k { font-size: 10.5px; color: var(--fg-3); }
.wffield input, .wffield select, .wffield textarea { width: 100%; font-size: 12px; }

/*
 * The store, in two halves: what happened, and what it was about.
 *
 * Neither is an appendix to the other, so neither gets the leftovers — the
 * list of captured requests and the globe of everything they mention take one
 * half each. Under 1100px they stack, list first: a 200px sphere is a
 * decoration, and the list is the half somebody can still use on a phone.
 */
.storegrid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: start;
}
@media (max-width: 1100px) { .storegrid { grid-template-columns: 1fr; } }
.storegrid > .card { min-width: 0; }
.cloudcard h3 { flex-wrap: wrap; gap: 6px; }
/*
 * Inside a half-width column the request and the response go under each
 * other. Asked of the CARD rather than the window, because the same list is
 * full width on a phone and half width beside the globe on a desktop, and
 * only the card knows which.
 */
.strow { container-type: inline-size; }
@container (max-width: 640px) {
  .samplepair { grid-template-columns: 1fr; }
}

.strow { border-top: 1px solid var(--line); }
.strow:first-child { border-top: 0; }
/*
 * One captured request, in as few things as it can be said in.
 *
 * The row used to carry nine — time, service, method, endpoint, status,
 * duration, bytes in, bytes out and a "traced" badge — and on a phone they
 * wrapped into four lines of confetti. What is left is when, where, what, how
 * it ended and how much travelled; below 520px of CARD width (not window
 * width: the same list is full width on a phone and half width beside the
 * globe) it folds into two lines, the endpoint first.
 */
.sthead {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto auto auto;
  align-items: center; gap: 6px 10px; padding: 8px 4px; cursor: pointer;
  border-radius: 6px;
}
.sthead:hover { background: var(--panel-2); }
.strow.open > .sthead { background: var(--panel-2); }
.st-r { min-width: 0; }
.st-b { white-space: nowrap; font-variant-numeric: tabular-nums; }
.st-t { white-space: nowrap; }
.st-svc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 18ch; }
@container (max-width: 520px) {
  .sthead {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: 'm r s' 't b svc';
    row-gap: 2px;
  }
  .st-m { grid-area: m; }
  .st-r { grid-area: r; }
  .st-s { grid-area: s; }
  .st-t { grid-area: t; }
  .st-b { grid-area: b; justify-self: start; }
  .st-svc { grid-area: svc; justify-self: end; }
}
.sthead .truncate { max-width: 42ch; }
.stbody { padding: 4px 4px 12px; }

.samplerow { padding: 10px 0; border-top: 1px solid var(--line); }
.samplerow:first-child { border-top: 0; padding-top: 0; }
.samplepair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 6px; }
.samplepair .k { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); }
.samplepair pre {
  margin: 4px 0 0; padding: 8px 10px; max-height: 260px; overflow: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  white-space: pre-wrap; word-break: break-word;
}
@media (max-width: 760px) { .samplepair { grid-template-columns: 1fr; } }

.advlist { display: flex; flex-direction: column; }
.advrow {
  display: grid; grid-template-columns: minmax(240px, 2fr) minmax(160px, 1fr) auto;
  gap: 12px; align-items: start; padding: 10px 0; border-top: 1px solid var(--line);
}
.advrow:first-child { border-top: 0; }
.advgain { color: var(--fg-2); }
.advdo { white-space: nowrap; }
/* A row we cannot act on yet reads as a note, not as a task. */
.advrow.soon .advgain { color: var(--fg-3); }
@media (max-width: 760px) {
  .advrow { grid-template-columns: 1fr; }
}
.patharrow { align-self: center; color: var(--fg-3); font-size: 14px; padding-top: 14px; }

.grid { display: grid; gap: 14px; }
/* min(100%, X) so a card narrower than its own minimum shrinks instead of
   overflowing the page — the failure this pattern exists to avoid. */
.g2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); }
.g3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.g4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); }
.g6 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr)); }
/* Rows that wrap by content rather than by a card count: the overview's four
   charts and its seven breakdowns. */
.cards { grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr)); }
.cards.narrow { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
/*
 * Host tiles stop growing at half the row.
 *
 * `auto-fit` with a `1fr` maximum makes two tiles share a 2500px row as two
 * 1240px cards — a tile whose content is a name, two gauges and a sparkline,
 * stretched to a width nothing in it uses. Capping the track at 640px keeps
 * them at a readable size and lets a third and fourth machine join the row
 * instead of the existing two inflating.
 *
 * `auto-fill`, not `auto-fit`: with a maximum in play auto-fit would collapse
 * the empty tracks and hand the leftover space back to the tiles, which is the
 * very thing being capped. justify-content keeps the row from drifting.
 */
.grid.cards.hosts {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
}
/*
 * `minmax(0, 1fr)`, not `1fr`.
 *
 * A bare `1fr` is `minmax(auto, 1fr)`, and `auto` there means min-content: the
 * column refuses to become narrower than its widest child. A card holding a
 * nine-column table therefore stretched to 555px inside a 390px screen, and the
 * table's own `overflow-x` never got the chance to do anything, because the box
 * it was supposed to scroll inside had simply grown instead.
 */
@media (max-width: 800px) { .grid { grid-template-columns: minmax(0, 1fr) !important; } }
/*
 * Except the stat tiles, which pair up.
 *
 * The rule above exists so a card holding a wide table cannot force the page
 * wider than a phone — a real failure, and worth the !important. But a KPI is
 * a label and a short number, and one per row turned the top of every page
 * into four screens of scrolling before the content started. Measured on a
 * 390px viewport: the Problems page began 620px down.
 *
 * Two per row still leaves ~180px each, which is wider than any of these
 * numbers needs, and the single-column rule still governs everything else.
 */
@media (max-width: 800px) {
  .grid.g4:has(.kpi), .grid.g6:has(.kpi) {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }
}
@media (max-width: 380px) {
  .grid.g4:has(.kpi), .grid.g6:has(.kpi) {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}

/*
 * Two columns on a wide screen, one on a narrow one.
 *
 * Measured on a 1920px display: a ticket's five cards each ran the full 1644px
 * while their content needed at most 1000px — 640px of empty card, five rows
 * deep, so the reader scrolls past nothing. The security page's scanning table
 * needed 548px of its 1610px, and Machines 615px.
 *
 * The breakpoint is 1240px rather than a round number because that is where two
 * 560px columns plus the gap and the sidebar stop fitting. Below it the rule
 * does nothing at all and every page keeps the single-column layout it has now.
 *
 * `align-items: start` matters: without it a short card stretches to the height
 * of the tall one beside it, which looks like a rendering fault rather than a
 * layout.
 */
.pair { display: grid; gap: 14px; align-items: start; }
@media (min-width: 1240px) {
  .pair { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  /* A card that genuinely needs the room says so and keeps the whole row. */
  .pair > .wide { grid-column: 1 / -1; }
}

/*
 * .flow — the same two columns, but the cards pack rather than sit in rows.
 *
 * A plain grid puts each pair of cards on a shared ROW, so a short card beside
 * a tall one leaves a hole the height of the difference: on a ticket the right
 * column stood empty next to the evidence table. CSS columns pack instead, so
 * the second column starts where the first one's card ended.
 *
 * break-inside: avoid is what keeps a card from being sliced across the column
 * boundary, which is the one way this layout can look broken.
 *
 * A card that needs the whole width cannot take part in a column flow, so
 * .flow is used only where every card is a fragment; anything full-width stays
 * outside the container. The ticket puts its chart and timeline outside for
 * exactly that reason.
 */
/*
 * A chart never pushes the page wider than the screen.
 *
 * timeChart draws with overflow:visible so a label at the edge is not clipped,
 * and falls back to a 900px canvas when its box has not been laid out yet.
 * Inside a two-column container on a phone that fallback stuck: a 553px SVG in
 * a 390px viewport, and the whole page scrolled sideways.
 *
 * The SVG already scales with width:100%; this stops it claiming more than its
 * parent regardless of what the fallback drew, so a late resize corrects the
 * picture instead of the page.
 */
.card svg { max-width: 100%; }
/*
 * And the box the chart draws into.
 *
 * Capping the SVG alone was not enough: timeChart's fallback canvas made the
 * wrapping div 584px wide inside a 390px viewport, and the SVG's width:100% of
 * that oversized parent was still 584. min-width:0 lets the div be as narrow
 * as the column, which is what makes the SVG's own cap effective — measured on
 * an iPhone viewport, body.scrollWidth 584 -> 390.
 *
 * min-width:auto on a grid or flex child means min-content, which is precisely
 * the rule that let the chart push the page wider than the screen.
 */
.pair > *, .flow > *, .trio > *, .card > div { min-width: 0; }

/*
 * .trio — three abreast on a really wide screen, two below that, one on a
 * phone. Used where a card holds three pieces that each stand on their own:
 * the security page's map, its hourly chart and its table of sources.
 *
 * The breakpoints are measured, not chosen. The sources table needs 548px
 * (404px of column ink plus the cells' own padding), which is the widest floor
 * of the three — a 24h bar chart stays readable at about 380px and the world
 * outline turns to a smudge below roughly 420px. Three 548px columns plus two
 * 14px gaps is 1672px inside the card; add the card's padding, the page's, and
 * the 236px sidebar and the layout needs a 1980px viewport. Hence 1980px for
 * three and the existing 1240px for two.
 *
 * Below 1240px it is a single column, like everything else.
 */
.trio { display: grid; gap: 14px; align-items: start; }
/*
 * Two columns only once each is worth having.
 *
 * This split at 1240px, and measured on a 1500px screen that gave the world
 * map 379px wide — every country in the width of a phone, beside a chart
 * squeezed to match. A map under roughly 480px says nothing the country table
 * next to it does not say better, so the second column waits for the room.
 */
@media (min-width: 1560px) {
  .trio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
@media (min-width: 1980px) {
  .trio { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.stack { display: flex; flex-direction: column; gap: 14px; }
/*
 * A stack owns the spacing between its own children.
 *
 * The blocks it holds are reused on pages that are not stacked, so several
 * carry a bottom margin of their own — the filter bar does — and inside a
 * stack that margin lands on top of the gap: the page then reads 14, 28, 14
 * and looks arbitrary rather than spaced.
 *
 * The column flow needs the opposite correction. Its cards space themselves
 * with a bottom margin, and the last card's margin is inside the flow's own
 * box, so the gap after the flow measures double. Only above 1240px, where
 * the flow is a multicol; below that it is a grid with its own gap and no
 * child margins to compensate for.
 */
.stack > * { margin-bottom: 0; }
@media (min-width: 1240px) { .stack > .flow { margin-bottom: -14px; } }
.flow { column-gap: 14px; }
.flow:has(> :only-child) { column-count: 1; }
@media (min-width: 1240px) {
  .flow { column-count: 2; }
  .flow > * { break-inside: avoid; margin-bottom: 14px; display: block; }
}
@media (max-width: 1239px) {
  .flow { display: grid; gap: 14px; }
}

/*
 * A card whose content is narrow prose.
 *
 * Running text already keeps a 90ch measure, so on a wide screen the card was
 * mostly empty to the right of it. This lets such a card sit at its natural
 * width in a pair instead of being stretched.
 */
/*
 * A map that is evidence on a page, not the page itself.
 *
 * geoMap sizes with height:auto, so the world's own 2:1 proportions turn a
 * 1610px card into a 500px-tall map — half a screen before the reader reaches
 * the numbers beside it. Capping the height and letting the SVG letterbox
 * inside keeps the shape honest (no squashing) while the card stays a card.
 *
 * The full-page World map deliberately has no cap: there the map IS the page.
 */
.mapcap svg { max-height: 320px; height: auto; }
/*
 * In a three-column row the map may be taller: it is sharing the height of a
 * 488px table, so 320px left the column short. The cap still exists — an
 * uncapped world map in a 922px column would be 460px tall.
 */
@media (min-width: 1980px) { .trio .mapcap svg { max-height: 420px; } }
@media (max-width: 900px) { .mapcap svg { max-height: 240px; } }

/*
 * A map beside the table that explains it.
 *
 * 1fr : 3fr, so the map takes a quarter and the country table the rest. The
 * map answers "roughly where", which needs shape and not size; the table
 * answers "how many from where", which needs columns. Stacking them made the
 * map full-bleed — a 3000px world outline above a table pushed below the
 * fold — and the map gained nothing from any of that width.
 *
 * Below 1100px they stack, because a quarter of a laptop is not a map.
 */
.geosplit { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
@media (min-width: 1100px) {
  /*
   * A quarter of the card was right when the card had the page to itself. It
   * now shares the row with the calls, so the map takes a larger share of a
   * smaller card — below about 1500px the two would each be too narrow to
   * read, and the map goes above the table instead.
   */
  .geosplit { grid-template-columns: minmax(0, 1fr); }
}
@media (min-width: 1500px) {
  .geosplit { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
}

.card.tight { max-width: 900px; }
@media (max-width: 1239px) { .card.tight { max-width: none; } }

.kpi { padding: 14px 16px; }
.kpi .label {
  font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--fg-3); font-weight: 600;
}
.kpi .value {
  font-size: 27px; font-weight: 650; letter-spacing: -.02em; margin-top: 4px;
  line-height: 1.15;
}
.kpi .sub { font-size: 12px; color: var(--fg-3); margin-top: 3px; }
.kpi .value.good { color: var(--good); }
.kpi .value.warn { color: var(--warn); }
.kpi .value.bad { color: var(--bad); }
/*
 * Tighter on a phone.
 *
 * The desktop tile is sized for a 250px column; on a 174px one the same
 * padding and 27px number left most of the tile empty, and four of them pushed
 * the actual content 592px down a 844px screen. This trims the chrome, not the
 * information — the label, the number and the sub-line all stay.
 */
@media (max-width: 800px) {
  .kpi { padding: 10px 12px; }
  .kpi .value { font-size: 21px; margin-top: 2px; }
  .kpi .sub { font-size: 11px; margin-top: 2px; }
  .kpi .label { font-size: 10px; }
}

.btn {
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--line-2);
  border-radius: 7px;
  padding: 7px 13px; cursor: pointer; font-weight: 500;
  transition: background .12s, border-color .12s;
}
.btn:hover { background: var(--panel-2); border-color: var(--fg-3); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.btn.primary:hover { background: color-mix(in srgb, var(--accent) 85%, var(--fg)); }
.btn.danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); }
.btn.danger:hover { background: var(--bad-bg); }
.btn.sm { padding: 4px 10px; font-size: 12px; }

input, select, textarea {
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--line-2); border-radius: 7px;
  padding: 7px 10px; font: inherit; width: 100%;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
  outline-offset: 1px;
  border-color: var(--accent);
}
label { display: block; font-size: 12px; color: var(--fg-2); margin: 12px 0 5px; font-weight: 500; }

.range { display: flex; gap: 2px; background: var(--panel-2); padding: 3px; border-radius: 8px; border: 1px solid var(--line); }
.range button {
  background: none; border: 0; color: var(--fg-2); padding: 5px 10px;
  border-radius: 5px; cursor: pointer; font-size: 12px; font-weight: 500;
}
.range button:hover { color: var(--fg); }
.range button.on { background: var(--accent); color: var(--accent-ink); font-weight: 600; }

.icon-btn {
  background: none; border: 1px solid var(--line-2); border-radius: 7px;
  color: var(--fg-2); cursor: pointer; padding: 5px 9px; font-size: 14px; line-height: 1;
}
.icon-btn:hover { background: var(--panel-2); color: var(--fg); }
/* The gear beside the application name in the topbar, and on its card. */
.icon-btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
.icon-btn.on { border-color: var(--accent); color: var(--accent); }

/*
 * Tables scroll inside their own box, never by widening the page.
 *
 * These have up to nine columns and no honest way to fit 390px. Unwrapped they
 * pushed the document to 885px, and the visible symptom was not "the table is
 * wide" — it was every card stopping short of the right edge, because the page
 * itself had become wider than the screen.
 *
 * `width: max-content` lets the table take the room its columns need, and
 * `min-width: 100%` keeps it filling the card on a desktop, where max-content
 * is usually narrower than the space available.
 */
.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; min-width: 0; }
.tablewrap table { width: max-content; min-width: 100%; }

table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--fg-3); font-weight: 650; padding: 8px 10px; border-bottom: 1px solid var(--line-2);
  white-space: nowrap; cursor: default;
}
td { padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr:hover { background: var(--panel-2); }
tbody tr.clickable { cursor: pointer; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); font-size: 12px; }
.truncate { max-width: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: 20px;
  font-size: 11px; font-weight: 600;
  background: var(--panel-2); color: var(--fg-2);
  border: 1px solid var(--line);
}
.badge.good { background: var(--good-bg); color: var(--good); border-color: color-mix(in srgb, var(--good) 30%, transparent); }
.badge.warn { background: var(--warn-bg); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.badge.bad  { background: var(--bad-bg);  color: var(--bad);  border-color: color-mix(in srgb, var(--bad) 30%, transparent); }
.badge.info { background: var(--info-bg); color: var(--info); border-color: color-mix(in srgb, var(--info) 30%, transparent); }

.muted { color: var(--fg-3); }
.small { font-size: 12px; }
.right { text-align: right; }
.row { display: flex; align-items: center; gap: 10px; }
.wrap { flex-wrap: wrap; }
/*
 * A label for a group of cards, where the page has more than one group.
 *
 * The process page draws two: cpu and memory, then the three request signals.
 * Without a line between them the five cards read as one undifferentiated wall
 * and the reader has to infer from the headings which two belong together.
 * Deliberately quieter than a card heading — it names a group, it is not a
 * thing in its own right.
 */
.secline {
  margin: 20px 0 10px;
  font-size: 13px;
  color: var(--fg-2);
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

.bars { display: flex; flex-direction: column; gap: 6px; }
.bars .b { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; font-size: 13px; }
.bars .track { position: relative; height: 24px; background: var(--panel-2); border-radius: 5px; overflow: hidden; }
.bars .fill { position: absolute; inset: 0 auto 0 0; background: var(--s1); opacity: .22; }
.bars .b:hover .fill { opacity: .34; }
.bars .txt { position: relative; padding: 3px 9px; line-height: 18px; z-index: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bars .n { font-variant-numeric: tabular-nums; color: var(--fg-2); font-size: 12px; font-weight: 600; }

.vital { border: 1px solid var(--line); border-radius: 8px; padding: 12px; background: var(--panel-2); }
.vital .name { font-size: 11px; color: var(--fg-3); letter-spacing: .06em; font-weight: 600; }
.vital .val { font-size: 22px; font-weight: 650; line-height: 1.15; margin-top: 2px; }
.vital .full { font-size: 11.5px; line-height: 1.35; color: var(--fg-2); margin-top: 3px; }
.vital .en { font-size: 10.5px; color: var(--fg-3); font-style: italic; }
.vital .stack { display: flex; gap: 2px; height: 6px; border-radius: 3px; overflow: hidden; margin-top: 9px; }
.vital .stack i { display: block; height: 100%; border-radius: 2px; }

abbr[title] { text-decoration: underline dotted var(--fg-3); text-underline-offset: 2px; cursor: help; }
th abbr[title] { text-decoration-color: var(--line-2); }

/* --------------------------------------------------------------- charts --- */

.legend {
  display: flex; gap: 14px; flex-wrap: wrap;
  font-size: 12px; color: var(--fg-2); margin-bottom: 8px;
}
.legend .key { display: inline-flex; align-items: center; gap: 6px; }
.legend .key i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.legend .key.line i { height: 3px; border-radius: 2px; width: 14px; }

.wf { font-family: var(--mono); font-size: 11px; }
.wf .row-wf {
  display: grid; grid-template-columns: minmax(180px, 320px) 1fr 62px 62px;
  gap: 10px; align-items: center; padding: 4px 0; border-bottom: 1px solid var(--line);
}
.wf .row-wf:hover { background: var(--panel-2); }
.wf .bar-wrap { position: relative; height: 15px; background: var(--panel-2); border-radius: 3px; }
.wf .seg { position: absolute; top: 0; height: 100%; }
.wf .lg { display: flex; gap: 12px; flex-wrap: wrap; margin: 4px 0 10px; font-size: 11px; color: var(--fg-2); }
.wf .lg i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }

.flag {
  display: inline-block; padding: 0 5px; border-radius: 4px; font-size: 9.5px;
  font-weight: 700; letter-spacing: .03em; vertical-align: 1px; cursor: help;
  font-family: -apple-system, "Segoe UI", sans-serif;
}
.flag.good { background: var(--good-bg); color: var(--good); }
.flag.warn { background: var(--warn-bg); color: var(--warn); }
.flag.bad  { background: var(--bad-bg); color: var(--bad); }

.geo { position: relative; }
.geo svg { border-radius: 8px; background: var(--ocean); display: block; }
.geo-legend {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 10px; font-size: 12px; color: var(--fg-2);
}
.geo-legend .sw { display: inline-flex; align-items: center; gap: 5px; }
.geo-legend .sw i { display: inline-block; width: 18px; height: 11px; border-radius: 3px; }

.filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.filters .f { min-width: 150px; }
.filters label { margin: 0 0 4px; }

.drill { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--fg-3); font-weight: 500; }
.card h3 .drill { text-transform: none; letter-spacing: 0; }
.kpi.linked { cursor: pointer; transition: border-color .12s, box-shadow .12s; }
.kpi.linked:hover { border-color: var(--accent); box-shadow: var(--shadow-lg); }
.kpi.linked .label::after { content: ' →'; color: var(--accent); }
.vital.linked { cursor: pointer; }
.vital.linked:hover { border-color: var(--accent); }

.split { display: flex; gap: 2px; height: 7px; border-radius: 4px; overflow: hidden; margin-top: 6px; }
.split i { display: block; height: 100%; border-radius: 2px; }

.opp { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center;
       padding: 10px 12px; border-bottom: 1px solid var(--line); }
.opp:hover { background: var(--panel-2); }
.opp .save { font-variant-numeric: tabular-nums; font-weight: 650; color: var(--good); white-space: nowrap; }

.timeline { position: relative; padding-left: 22px; }
.timeline::before { content: ''; position: absolute; left: 6px; top: 4px; bottom: 4px; width: 2px; background: var(--line); }
.tl-item { position: relative; padding: 6px 0 6px 4px; display: grid; grid-template-columns: 66px 1fr; gap: 10px; align-items: baseline; }
.tl-item::before {
  content: ''; position: absolute; left: -20px; top: 12px;
  width: 10px; height: 10px; border-radius: 50%; background: var(--fg-3); border: 2px solid var(--panel);
}
.tl-item.view::before { background: var(--s1); }
.tl-item.error::before { background: var(--bad); }
.tl-item.request_failed::before { background: var(--warn); }
.tl-item.rage::before, .tl-item.dead::before { background: var(--serious); }
.tl-item.custom::before { background: var(--s3); }
.tl-item .t { color: var(--fg-3); font-size: 11px; font-family: var(--mono); }
.tl-item .d { font-size: 12px; color: var(--fg-3); }

pre.stack {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 7px;
  padding: 12px; overflow-x: auto; font-family: var(--mono); font-size: 12px;
  color: var(--fg-2); margin: 0; line-height: 1.55; max-height: 340px;
}

.snippet {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 7px;
  padding: 12px; font-family: var(--mono); font-size: 12px; color: var(--fg);
  white-space: pre-wrap; word-break: break-all; user-select: all;
}

.modal-bg {
  position: fixed; inset: 0; background: color-mix(in srgb, var(--fg) 45%, transparent);
  display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px;
}
.modal {
  background: var(--panel); border: 1px solid var(--line-2); border-radius: 12px;
  padding: 22px; width: 100%; max-width: 620px; max-height: 88vh; overflow-y: auto;
  box-shadow: var(--shadow-lg);
}
.modal h2 { margin: 0 0 6px; font-size: 18px; }


.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 16px; }
.tabs button {
  background: none; border: 0; color: var(--fg-2); padding: 9px 14px; cursor: pointer;
  border-bottom: 2px solid transparent; font-weight: 500;
}
.tabs button:hover { color: var(--fg); }
.tabs button.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }

.empty { padding: 40px; text-align: center; color: var(--fg-3); }
.spin {
  display: inline-block; width: 14px; height: 14px;
  border: 2px solid var(--line-2); border-top-color: var(--accent);
  border-radius: 50%; animation: sp .7s linear infinite;
}
@keyframes sp { to { transform: rotate(360deg); } }

.tooltip {
  position: fixed; pointer-events: none; z-index: 200;
  background: var(--panel); border: 1px solid var(--line-2); border-radius: 8px;
  padding: 9px 11px; font-size: 12px; color: var(--fg);
  box-shadow: var(--shadow-lg); white-space: nowrap;
}
.tooltip .k { color: var(--fg-3); margin-right: 8px; }
.tooltip b { font-variant-numeric: tabular-nums; }

.toast {
  position: fixed; bottom: 20px; right: 20px; z-index: 300;
  background: var(--panel); border: 1px solid var(--line-2); border-left: 3px solid var(--good);
  border-radius: 8px; padding: 11px 16px; box-shadow: var(--shadow-lg);
  animation: slide .2s ease;
}
.toast.err { border-left-color: var(--bad); }
@keyframes slide { from { transform: translateY(8px); opacity: 0; } }

.devices summary, .glossary summary {
  cursor: pointer; font-size: 13px; font-weight: 650; color: var(--fg-2);
  list-style: none; display: flex; align-items: center; gap: 8px;
}
.devices summary::before, .glossary summary::before { content: '▸'; color: var(--fg-3); }
.devices[open] summary::before, .glossary[open] summary::before { content: '▾'; }
.devices summary::-webkit-details-marker, .glossary summary::-webkit-details-marker { display: none; }
.kv { padding: 9px 11px; background: var(--panel-2); border-radius: 7px; min-width: 0; }
.kv .k { font-size: 11px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .05em; font-weight: 600; }
.kv .v { font-size: 13px; margin-top: 2px; overflow-wrap: anywhere; }
.glossary .term { padding: 10px 12px; background: var(--panel-2); border-radius: 7px; }
.glossary .term .hu { color: var(--fg); font-size: 13px; margin: 2px 0 3px; }

/* ------------------------------------------------------------- leaflet --- */

.leaflet-host .leaflet-container {
  background: var(--ocean);
  border-radius: 8px;
  font: inherit;
  outline: none;
  /*
   * The map must be one flat layer in the page, not seven.
   *
   * Leaflet gives its internal panes z-indexes from 200 to 1000 — tiles 200,
   * overlays 400, markers 600, popups 700, controls 1000 — and because
   * .leaflet-container was position:relative with z-index:auto it opened NO
   * stacking context of its own. Those panes therefore competed directly with
   * the page's own layers, where the off-canvas navigation drawer sits at 60.
   * On a phone, with the drawer open over the world map, the bottom of the
   * menu was painted over by the map: measured on a real iPhone viewport, the
   * lowest 11% of the drawer hit-tested to an SVG country path rather than to
   * the menu, which covers Hosts and the Problems and Topology buttons.
   *
   * isolation:isolate makes the container a stacking context, so every pane
   * inside it is ordered against its siblings and the whole map takes part in
   * the page as a single element. It changes nothing about how the map looks.
   */
  isolation: isolate;
  /*
   * And with the map now a single layer, that layer is given a rank below the
   * drawer and its scrim (60 and 50) and below the sticky topbar (20), so it
   * stays inside its card in every direction.
   */
  z-index: 0;
  position: relative;
}

/*
 * OSM's standard tiles are drawn for a light page, so dark mode filters them
 * locally rather than pulling a second tile set (another external dependency).
 *
 * Darkening, NOT inverting. OSM land is near-white, so invert() lands it on
 * #0b0d12 — 1.30:1 against the card, which is why the inactive world was a
 * black smear — and hue-rotate turned the sea brown. Plain brightness keeps
 * land at #5c5b59 (2.21:1) and water blue, both legible.
 */
:root[data-theme='dark'] .leaflet-host .leaflet-tile-pane,
:root:not([data-theme='light']) .leaflet-host .leaflet-tile-pane {
  filter: brightness(.38) saturate(.75) contrast(1.05);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) .leaflet-host .leaflet-tile-pane { filter: none; }
}

.leaflet-host .leaflet-control-attribution {
  background: color-mix(in srgb, var(--panel) 86%, transparent);
  color: var(--fg-3);
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 5px 0 0 0;
}
.leaflet-host .leaflet-control-attribution a { color: var(--fg-2); }

.leaflet-host .leaflet-control-zoom a {
  background: var(--panel);
  color: var(--fg);
  border-color: var(--line-2);
}
.leaflet-host .leaflet-control-zoom a:hover { background: var(--panel-2); }

.leaflet-rum-tip.leaflet-tooltip {
  background: var(--panel);
  border: 1px solid var(--line-2);
  color: var(--fg);
  box-shadow: var(--shadow-lg);
  border-radius: 8px;
  padding: 8px 10px;
  font-size: 12px;
}
.leaflet-rum-tip.leaflet-tooltip::before { display: none; }
.leaflet-rum-tip .k { color: var(--fg-3); }
.leaflet-rum-tip b { font-variant-numeric: tabular-nums; }

/* -------------------------------------------------------------- replay --- */

/*
 * A window onto the recording, sized in JS to whichever is smaller — the
 * picture at the current zoom, or the space. No scrollbars: they ate two edges
 * of an already small window, and what is outside is reached with the wheel or
 * by dragging instead. No min-height either: a floor taller than the recording
 * puts empty panel above and below it, which is the same "why is it cut off"
 * question in the other axis.
 */
.replay-stage {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  position: relative;
  margin-inline: auto;
  max-width: 100%;
}

/*
 * The sandboxed replay iframe swallows pointer and wheel events, so panning is
 * bound to a transparent sheet over it. It never intercepts anything else:
 * the replay is not interactive.
 */
.replay-grab { position: absolute; inset: 0; z-index: 2; }
.replay-grab.pannable { cursor: grab; }
.replay-grab.pannable.dragging { cursor: grabbing; }

.replay-zoom { width: 96px; padding: 0; accent-color: var(--accent); }
/* The canvas IS the recording: replay.js sets it to the recorded viewport in
   pixels and puts the zoom and the pan on it as one transform. */
.replay-canvas { position: relative; transform-origin: 0 0; }
.replay-canvas .replayer-wrapper { transform-origin: top left; }
.replay-canvas iframe { border: 0; background: white; }

.replay-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 10px;
}
.replay-time, .replay-total { font-size: 12px; color: var(--fg-2); min-width: 52px; }
.replay-total { text-align: right; }
.replay-speed { width: auto; padding: 4px 8px; font-size: 12px; }

.replay-track {
  position: relative; flex: 1; min-width: 160px; height: 22px;
  cursor: pointer; display: flex; align-items: center;
}
.replay-track::before {
  content: ''; position: absolute; left: 0; right: 0; height: 6px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 3px;
}
.replay-track:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.replay-played {
  position: absolute; left: 0; height: 6px; width: 0;
  background: var(--accent); border-radius: 3px;
}
.replay-marks { position: absolute; inset: 0; pointer-events: none; }
.replay-mark {
  position: absolute; top: 3px; width: 3px; height: 16px; padding: 0;
  border: 0; border-radius: 2px; background: var(--fg-3);
  cursor: pointer; pointer-events: auto; transform: translateX(-1px);
}
.replay-mark:hover { transform: translateX(-1px) scaleX(2); }
.replay-mark.error { background: var(--bad); }
.replay-mark.rage, .replay-mark.dead { background: var(--serious); }
.replay-mark.view { background: var(--s1); }

/* ------------------------------------------------------------- mobile --- */

/*
 * Below 800px the sidebar becomes a drawer instead of disappearing.
 *
 * It used to be `display: none` here, which left the navigation unreachable:
 * whatever view you landed on was the only one you could get to. The panel is
 * now off-canvas and slides in over the content, with a scrim behind it.
 *
 * The open state is a class on <html> so CSS owns the transition and the app
 * code never has to know where the breakpoint is.
 */
.menu-btn, .drawer-close { display: none; }
.drawer-scrim { display: none; }

@media (max-width: 800px) {
  .menu-btn { display: inline-flex; }
  .drawer-close { display: inline-flex; margin-left: auto; }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    height: 100%;
    z-index: 60;
    transform: translateX(-100%);
    transition: transform .22s ease;
    box-shadow: var(--shadow-lg);
    /* Wide enough to read, never edge to edge: the strip of visible page is
       what tells you the drawer is a layer you can dismiss. */
    width: min(300px, 84vw);
    flex-basis: min(300px, 84vw);
  }
  :root.drawer-open .sidebar { transform: translateX(0); }

  .drawer-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
    background: color-mix(in srgb, black 46%, transparent);
    opacity: 0;
    pointer-events: none;
    transition: opacity .22s ease;
  }
  :root.drawer-open .drawer-scrim { opacity: 1; pointer-events: auto; }

  .content { padding: 14px; }
  .topbar { padding: 10px 14px; gap: 8px; }
  .topbar h1 { font-size: 16px; }
  /*
   * Two rows, and who sits on which is deliberate.
   *
   * The spacer becomes a line break, which used to sweep EVERYTHING after it —
   * theme, scope, range and the account button — onto a left-packed second
   * row, so the user icon sat stranded mid-bar. Identity belongs in the top
   * right corner of a mobile app bar the way it does in every app the visitor
   * already uses, so the account and theme buttons are ordered before the
   * break and pushed right, and only the page controls (scope, range, auto)
   * wrap to the second row.
   */
  .topbar > * { order: 5; }
  .topbar .menu-btn { order: 0; }
  .topbar h1 { flex: 1 1 auto; order: 1; }
  .topbar .theme-btn { order: 2; margin-left: auto; }
  .topbar .usermenu { order: 3; }
  .topbar .spacer { flex-basis: 100%; height: 0; order: 4; }
  .topbar select.scope { max-width: none; flex: 1 1 120px; }
  .topbar .range { flex: 1 1 100%; }
  .range button { flex: 1 1 0; min-width: 0; padding-inline: 6px; text-align: center; }
  .range button.live { justify-content: center; }

  /* Control rows are flex with no wrapping, so on a narrow screen they set a
     hard minimum width for the whole page — the session filters pinned it at
     373px, which overflowed a 360px phone. Let them wrap instead. */
  .range { flex-wrap: wrap; }
  .row, .row.wrap { flex-wrap: wrap; }

  /* 380px of truncation does not fit on a 390px screen once padding and the
     other columns are counted. */
  .truncate { max-width: 52vw; }
}

/* Respect a reduced-motion preference: the drawer still works, it just
   appears rather than slides. */
@media (prefers-reduced-motion: reduce) {
  .sidebar, .drawer-scrim { transition: none; }
}

/* ------------------------------------------------------ landing / login --- */

/*
 * Signed out: the intro video is the whole background, dimmed, with the
 * sign-in card floating on it.
 *
 * The card stays OPAQUE on purpose. Frosted glass over video is the obvious
 * thing to reach for and it is the wrong thing here: the text contrast would
 * then depend on whichever frame happened to be behind it, which is
 * unmeasurable and changes 30 times a second. Opaque card means the label and
 * input contrast is exactly what tools/contrast.mjs already verifies for every
 * other surface in the product, video or no video.
 */
.landing {
  flex: 1;
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
}

.landing-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--bg);
}
.landing-bg video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/*
 * The veil is what makes it a background rather than a feature. It is the page
 * background colour at high opacity, so the video reads as texture in both
 * themes and never competes with the form.
 */
.landing-veil {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
}

.landing-form {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 420px;
  padding: 32px;
  box-shadow: var(--shadow-lg);
}

@media (max-width: 560px) {
  .landing { padding: 16px; }
  .landing-form { padding: 24px 20px; }
}

/*
 * Reduced motion: no looping video. A still frame would be defensible, but the
 * honest answer to "do not animate" is to drop it, not to keep it and hope.
 */
@media (prefers-reduced-motion: reduce) {
  .landing-bg video { display: none; }
}

/* --------------------------------------------------------- sortable th --- */

/*
 * Only columns with a reachable underlying value get the affordance. A numeric
 * column with no `key` or `sort` stays a plain header on purpose: sorting it on
 * the rendered text would order "1.2 s" before "950 ms", and a wrong order that
 * looks right is worse than no sorting at all.
 */
th.sortable {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
th.sortable:hover { color: var(--fg); }
th.sortable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 4px;
}
th.sorted { color: var(--accent); }

.sort-arrow {
  display: inline-block;
  margin-left: 5px;
  /* The idle indicator is deliberately faint: 24 headers all shouting "sort
     me" is noise, but an invisible affordance is not discoverable either. */
  opacity: 0.35;
  font-size: 10px;
}
th.sortable:hover .sort-arrow { opacity: 0.75; }
th.sorted .sort-arrow { opacity: 1; }

/* ------------------------------------------------------- traffic matrix --- */

/* A control that lives in a card header, where the full-width default for
   inputs would break the header row. */
select.inline, input.inline { width: auto; padding: 4px 8px; font-size: 12px; }

.dotmatrix { overflow-x: auto; padding: 4px 0 2px; }
.dotmatrix svg { display: block; }
.dotmatrix .gx { fill: var(--fg-2); font-size: 12px; font-weight: 650; }
.dotmatrix .gy { fill: var(--fg-3); font-size: 11px; }
.dotmatrix circle { transition: r .1s ease; }
.dotmatrix .dm-cell { cursor: pointer; }
.dotmatrix .dm-cell:hover { stroke: var(--fg); stroke-width: 1.5; }

/* ------------------------------------------------------------ topology --- */

.topology { overflow-x: auto; position: relative; }
.topology svg { display: block; min-width: 720px; }

/* The backdrop: a scale for the eye, not decoration. Every node on a ring is
   the same distance from the application. */
.tp-ring { fill: none; stroke: var(--line); opacity: .35; }
.tp-spoke { stroke: var(--line); opacity: .18; }

.tp-edge { stroke: var(--line-2); opacity: .5; }
.tp-edge.warn { stroke: var(--warn); opacity: .6; }
.tp-edge.bad { stroke: var(--bad); opacity: .7; }

/* The moving copy. The only thing here that is not a measurement, doing one
   job: saying which way the traffic goes. */
.tp-flow {
  stroke: var(--s1);
  stroke-dasharray: 3 26;
  stroke-linecap: round;
  opacity: .9;
  animation: tp-dash linear infinite;
}
.tp-flow.warn { stroke: var(--warn); }
.tp-flow.bad { stroke: var(--bad); }
@keyframes tp-dash { to { stroke-dashoffset: -290; } }

.tp-app rect { fill: var(--panel-2); stroke: var(--accent); stroke-width: 1.5; filter: url(#tp-glow); }
.tp-app-name { fill: var(--fg); font-size: 15px; font-weight: 650; }
.tp-app-host { fill: var(--fg-2); font-size: 12px; font-family: var(--mono); }
.tp-app-sub { fill: var(--fg-3); font-size: 11px; }
.tp-orbit {
  fill: none; stroke: var(--accent); opacity: .3;
  transform-box: view-box; transform-origin: center;
  animation: tp-spin linear infinite;
}
.tp-orbit.a { stroke-dasharray: 34 300; animation-duration: 14s; }
.tp-orbit.b { stroke-dasharray: 12 120; animation-duration: 24s; animation-direction: reverse; opacity: .2; }
@keyframes tp-spin { to { transform: rotate(360deg); } }

/* Fill says whose it is, the ring says whether it is healthy — two questions,
   two channels, so neither has to be read off the other. */
.tp-node circle { fill: var(--s1); stroke: var(--panel); stroke-width: 2; cursor: pointer; filter: url(#tp-glow); }
.tp-node.third circle { fill: var(--s2); }
.tp-node.warn circle { stroke: var(--warn); }
.tp-node.bad circle { stroke: var(--bad); stroke-width: 3; }
.tp-node .tp-core { fill: var(--panel); stroke: none; filter: none; opacity: .55; }
.tp-node:hover circle { stroke: var(--fg); }
.tp-node .tp-halo {
  fill: none; stroke: var(--bad); stroke-width: 2; filter: none;
  transform-box: fill-box; transform-origin: center;
  animation: tp-pulse 1.9s ease-out infinite;
}
@keyframes tp-pulse {
  0%   { transform: scale(1);   opacity: .75; }
  100% { transform: scale(2.4); opacity: 0; }
}
.tp-label { fill: var(--fg-2); font-size: 12px; pointer-events: none; }
.tp-col { fill: var(--fg-3); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }

.tp-legend { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; row-gap: 4px; }
.tp-legend .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 3px; vertical-align: -1px; }
.tp-legend .dot.first { background: var(--s1); }
.tp-legend .dot.third { background: var(--s2); }
.tp-legend .dot.bad { background: none; border: 2px solid var(--bad); }

/* Everything above that moves, stops. */
@media (prefers-reduced-motion: reduce) {
  .tp-flow, .tp-orbit, .tp-node .tp-halo { animation: none; }
  .tp-flow { opacity: .35; }
  .tp-node .tp-halo { opacity: .5; }
}

/* ---------------------------------------------------------- nav groups --- */

.navgroup .sec.toggle {
  display: flex; align-items: center; gap: 6px; width: 100%;
  background: none; border: 0; padding: 0; margin: 14px 0 4px;
  font: inherit; font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--fg-3); cursor: pointer; text-align: left;
}
.navgroup .sec.toggle:hover { color: var(--fg-2); }
.navgroup .chev { transition: transform .12s ease; display: inline-block; }
.navgroup.closed .chev { transform: rotate(-90deg); }
.navgroup.closed .navitems { display: none; }
.navempty { font-size: 12px; color: var(--fg-3); padding: 2px 10px 6px; line-height: 1.45; }

/*
 * Topology sits below both groups on purpose: it is the one page that is about
 * the whole system rather than one side of it, so it belongs to neither. The
 * accent treatment is the only one in the nav, which is what makes it findable
 * — a second highlighted item would cancel the first.
 */
/*
 * Scoped to the class, not to the nav: this link lives in the sidebar footer
 * now, and every rule here used to start with `.nav a`.
 */
.navtopo {
  display: flex; align-items: center;
  padding: 8px 11px;
  border-radius: 7px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line));
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  color: var(--accent);
  font-weight: 600;
}
.navtopo:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.navtopo.active { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--fg); }
.navtopo .spark {
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
  display: inline-block; margin-right: 9px; vertical-align: 1px;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent);
  animation: nav-spark 2.4s ease-out infinite;
}
@keyframes nav-spark {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) { .navtopo .spark { animation: none; } }

/*
 * The two footer destinations sit together and must not read as one control,
 * so they are separated by a gap rather than by a divider line.
 */
.sidebar-foot { display: flex; flex-direction: column; gap: 8px; }

/*
 * Problems wears the state it reports, not the accent: at a glance the colour
 * IS the answer, so a quiet system is quiet and a live one is not.
 *
 * Quiet is the default — the same shape as Topology in the neutral ink, with
 * no pulse. Only `.live` (something open) takes a status colour, and only
 * `.crit` takes the critical one.
 */
.navtopo.problemlink {
  border-color: var(--line);
  background: transparent;
  color: var(--fg-2);
}
.navtopo.problemlink .spark { background: var(--fg-3); animation: none; box-shadow: none; }
.navtopo.problemlink:hover { background: var(--panel-2); color: var(--fg); }

.navtopo.problemlink.live {
  border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  color: var(--warn);
}
.navtopo.problemlink.live .spark {
  background: var(--warn);
  animation: nav-spark-warn 2.4s ease-out infinite;
}
.navtopo.problemlink.live .spark.crit { background: var(--bad); animation-name: nav-spark-bad; }
.navtopo.problemlink.live:has(.spark.crit) {
  border-color: color-mix(in srgb, var(--bad) 45%, var(--line));
  background: color-mix(in srgb, var(--bad) 10%, transparent);
  color: var(--bad);
}
.navtopo.problemlink.active { background: var(--panel-2); color: var(--fg); }
.navtopo.problemlink .badge { margin-left: auto; }

@keyframes nav-spark-warn {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--warn) 55%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@keyframes nav-spark-bad {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--bad) 55%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) { .navtopo.problemlink .spark { animation: none; } }

/* ---------------------------------------------------------- fact grids --- */

/*
 * Label over value, wrapping to however many columns the card turned out to
 * be. Used by the session's captured attributes and by the install summary.
 *
 * This block was deleted once, as collateral of removing a replay side panel
 * that happened to be defined next to it — and nothing failed, because missing
 * CSS does not throw. The two places that use it just quietly ran together as
 * "Collector addresshttp://…". Grep before deleting a neighbouring rule.
 */
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); gap: 10px 18px; font-size: 13px; }
.facts .k { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); margin-bottom: 2px; }
/*
 * A fact's value stays inside its own column.
 *
 * A trace id is 32 characters of hex with nowhere to break, and it printed
 * straight over the fact beside it. Grid children default to a minimum size of
 * their content, so the min-width is the actual fix; the wrap rule then lets
 * the hex fold instead of pushing.
 */
.facts > div { min-width: 0; }
.facts b { font-weight: 650; color: var(--fg); overflow-wrap: anywhere; }
.facts a { font-weight: 650; }

/* --------------------------------------------------- application cards --- */

/*
 * Six numbers per application, wrapping by width rather than by count: at three
 * cards across they are two rows of three, on a narrow screen two of two, and
 * on one wide card a single row. A fixed column count leaves a hole at every
 * width that fits a different number.
 */
.appstats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(50%, 96px), 1fr)); gap: 12px 18px; }
.appstat .k { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); }
.appstat .v { font-size: 19px; font-weight: 650; margin-top: 2px; }
.appstat .s { font-size: 11px; color: var(--fg-3); margin-top: 1px; }
.appstat .v.good { color: var(--good); }
.appstat .v.warn { color: var(--warn); }
.appstat .v.bad { color: var(--bad); }

/* ------------------------------------------------------- attribute rules --- */

/*
 * One rule per row: name, selector, identity, remove. The selector gets the
 * room because it is the part that is long and the part people check.
 */
.attrs { display: grid; gap: 8px; margin-top: 6px; }
.attr-row { display: grid; grid-template-columns: minmax(90px, 150px) minmax(0, 1fr) auto auto; gap: 8px; align-items: center; }
.attr-row input { width: 100%; }
.attr-row .a-sel { font-size: 12px; }
@media (max-width: 700px) {
  .attr-row { grid-template-columns: 1fr auto; grid-template-areas: 'name del' 'sel sel' 'id id'; }
  .attr-row .a-name { grid-area: name; }
  .attr-row .a-sel { grid-area: sel; }
  .attr-row .a-id { grid-area: id; }
  .attr-row .a-del { grid-area: del; }
}

/* ------------------------------------------------------------ security --- */

/*
 * Severity uses the reserved STATUS colours, not a categorical palette.
 * Severity is a state, not an identity, and the invariant is that a status
 * colour means one thing everywhere in the product — red is "this is bad"
 * whether it is a failing request or a critical advisory.
 */
.vuln {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 0;
  border-top: 1px solid var(--line);
  font-size: 13px;
}
.vuln > span:nth-child(3) { flex: 1; min-width: 220px; color: var(--fg-2); }

/* ------------------------------------------------------- host entities --- */

/*
 * A host card is a KPI tile that happens to be about a machine: the two
 * numbers that decide whether to look further (cpu, memory) as meters, the
 * last hour of cpu behind them, and the layers above it as plain counts.
 *
 * Meters rather than numbers alone because a percentage is a proportion and a
 * bar is read as one — 91% and 19% are one glance apart as bars and two
 * readings apart as text. The colour is the STATUS palette: a meter says
 * "this is fine / this needs looking at", which is a state, not an identity.
 */
.hostcard { cursor: pointer; transition: border-color .12s, box-shadow .12s; }
.hostcard:hover { border-color: var(--accent); box-shadow: var(--shadow-lg); }
.hostcard h3 { align-items: center; }
/*
 * The gear sits on the tile, which is itself a link. It is quiet until the
 * pointer is on the tile, because a row of settings icons competes with the
 * numbers that are the reason to look at the list.
 */
.hostcard .gear { opacity: 0; padding: 0 6px; line-height: 1.6; }
.hostcard:hover .gear, .hostcard .gear:focus-visible { opacity: 1; }

.meter { height: 6px; border-radius: 3px; background: var(--panel-2); overflow: hidden; margin-top: 5px; }
.meter > i { display: block; height: 100%; border-radius: 3px; background: var(--s1); transition: width .2s; }
.meter.warn > i { background: var(--warn); }
.meter.bad  > i { background: var(--bad); }

/* Label, value, meter — repeated for cpu and memory, side by side. */
.gauges { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; margin: 12px 0 4px; }
.gauge .k { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); }
.gauge .v { font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.15; }
.gauge .v small { font-size: 11px; font-weight: 500; color: var(--fg-3); margin-left: 4px; }

/* The stack: host → process → service, as three counts under the meters. */
.layers { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--line); font-size: 12px; color: var(--fg-2); }
.layers b { color: var(--fg); font-variant-numeric: tabular-nums; }

/*
 * No fixed height here: sparkline() sets its own via a style attribute, and a
 * rule with the same specificity but a later position was silently winning —
 * a 54px chart drawn into a 34px box, squashed by preserveAspectRatio:none.
 */
.spark { display: block; width: 100%; }

/*
 * A one-line filter, for when the choices are few and named.
 *
 * A select would hide them behind a click; these are hosts, and there are
 * three. The selected one is filled rather than merely coloured, so the state
 * survives being printed, being colourblind, and being glanced at.
 */
.chip {
  display: inline-block;
  padding: 3px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 12px;
  color: var(--fg-2);
  text-decoration: none;
  white-space: nowrap;
}
.chip:hover { border-color: var(--accent); color: var(--fg); }
.chip.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }

/* ------------------------------------------------------------- account --- */

/*
 * A disclosure button and a panel, top right.
 *
 * Not a hover menu: a hover menu cannot be opened by touch or by keyboard, and
 * this one holds the way out of the product. The panel is positioned against
 * the button rather than the page so it stays put when the topbar wraps.
 */
.usermenu { position: relative; }
.usermenu-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 210px; padding: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; gap: 2px;
}
.usermenu-panel[hidden] { display: none; }
.usermenu-who { padding: 8px 10px 10px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.usermenu-who .n { font-weight: 650; font-size: 13px; }
.usermenu-who .e { font-size: 12px; color: var(--fg-3); word-break: break-all; margin-bottom: 6px; }
.usermenu-panel > a,
.usermenu-panel > button {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; border: 0; border-radius: 7px;
  background: transparent; color: var(--fg-2);
  font: inherit; font-size: 13px; text-align: left; text-decoration: none; cursor: pointer;
}
.usermenu-panel > a:hover,
.usermenu-panel > button:not(:disabled):hover { background: var(--panel-2); color: var(--fg); }
.usermenu-panel > button:disabled { color: var(--fg-3); cursor: default; }
.usermenu-panel > button.soon { justify-content: space-between; }

/* The topology entry sits in the footer now, where the account used to be. */
.sidebar-foot { padding: 10px; }

/* ------------------------------------------------------- topology: servers */

/*
 * The machines are slabs, not bubbles.
 *
 * A domain on this map is a bubble whose size is traffic; a host is a fixed
 * rectangle carrying two meters. Different shapes because they are different
 * kinds of thing — nobody should have to read a legend to tell a dependency
 * from a machine — and a rectangle is also the only shape with room for
 * numbers.
 */
.tp-tier { stroke: var(--line); stroke-dasharray: 2 6; opacity: .7; }

.tp-host rect { fill: var(--panel-2); stroke: var(--line); stroke-width: 1; }
.tp-host:hover rect { stroke: var(--accent); }
.tp-host.stale rect { stroke: color-mix(in srgb, var(--warn) 55%, var(--line)); }
.tp-host-name { fill: var(--fg); font-size: 12.5px; font-weight: 650; }
.tp-host-sub { fill: var(--fg-3); font-size: 10px; }

.tp-meter-bg { fill: var(--line); opacity: .8; }
.tp-meter.ok { fill: var(--s1); }
.tp-meter.warn { fill: var(--warn); }
.tp-meter.bad { fill: var(--bad); }

/* Ownership, not flow: these machines report under this application's key.
   Drawn quietly and never animated — only measured traffic moves here. */
.tp-own { stroke: var(--line); stroke-width: 1.5; stroke-dasharray: 3 5; opacity: .8; }
.tp-runs { stroke: var(--line); stroke-width: 1.5; opacity: .9; }

.tp-svc circle { fill: color-mix(in srgb, var(--s1) 26%, transparent); stroke: var(--s1); stroke-width: 1.5; }
.tp-svc.warn circle { fill: color-mix(in srgb, var(--warn) 26%, transparent); stroke: var(--warn); }
.tp-svc.bad circle { fill: color-mix(in srgb, var(--bad) 26%, transparent); stroke: var(--bad); }
.tp-svc .tp-core { fill: var(--s1); stroke: none; }
.tp-svc.warn .tp-core { fill: var(--warn); }
.tp-svc.bad .tp-core { fill: var(--bad); }
.tp-svc { cursor: pointer; }
.tp-host { cursor: pointer; }

/* --------------------------------------------------------- trace detail --- */

/*
 * What one request did, as a waterfall.
 *
 * Its own class names: `.wf` is the browser-side resource waterfall, which is
 * a different measurement with a different anatomy, and sharing the styles
 * would tie two unrelated things together.
 *
 * The request itself is the first row and the frame for the rest; a span is
 * positioned by when it started, so the gaps are the service's own work. The
 * bars are thin and the numbers are beside them, because the number is what
 * gets read and the bar is what gets compared.
 */
.twf { display: flex; flex-direction: column; gap: 4px; font-size: 12px; }
/*
 * Wider names, because a row now carries a service badge as well as a route:
 * a chain of hops whose names are all cut off at the same width tells the
 * reader there were five of something.
 */
/*
 * Wider on both ends than it was: the name carries connectors, a service
 * badge and a route, and the right column now carries a duration and the
 * window it occupied.
 */
.twfrow { display: grid; grid-template-columns: minmax(230px, 3fr) 4fr minmax(104px, auto); gap: 10px; align-items: center; }
.twfname { font-family: var(--mono); font-size: 11px; color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.twfbar { position: relative; height: 14px; background: var(--panel-2); border-radius: 3px; overflow: hidden; }
.twfbar > i { position: absolute; top: 0; height: 100%; border-radius: 3px; background: var(--s2); min-width: 2px; }
.twfbar > i.self { background: var(--s1); }
.twfbar > i.http { background: var(--s2); }
.twfms { font-variant-numeric: tabular-nums; text-align: right; color: var(--fg-2); font-size: 11px; }

/*
 * A trace crosses processes, so the waterfall has rows from more than one.
 *
 * The row the reader arrived at is marked with a rule and a slightly stronger
 * ink rather than a fill: they are looking at a chain of many hops and need to
 * find their place in it without the mark competing with the bars, which are
 * the thing being compared.
 */

/* ------------------------------------------------------- the trace tree --- */

/*
 * Connectors, not indentation.
 *
 * Five levels across three processes is where padding alone stops working: the
 * reader ends up counting pixels to find out what called what. Each cell is one
 * level — a line running past, empty space, a tee, or the elbow that says this
 * was the last child.
 */
.twfname .tgs { display: inline-flex; flex: 0 0 auto; margin-right: -2px; }
.twfname .tg { display: inline-block; position: relative; width: 14px; height: 18px; vertical-align: middle; flex: 0 0 auto; }
.twfname .tg.line::before,
.twfname .tg.tee::before,
.twfname .tg.end::before {
  content: ""; position: absolute; left: 6px; top: 0; width: 1px; background: var(--line);
  height: 100%;
}
/* The elbow stops halfway: nothing below it belongs to that parent. */
.twfname .tg.end::before { height: 50%; }
.twfname .tg.tee::after,
.twfname .tg.end::after {
  content: ""; position: absolute; left: 6px; top: 50%; width: 7px; height: 1px; background: var(--line);
}

/* A hop or call that failed. The bar keeps its own colour; the name carries it. */
.twfrow.is-failed .twfname { color: var(--bad); }

/*
 * A ruler, and gridlines under the bars.
 *
 * Without them a bar's position is decoration: the reader sees that one call
 * started after another and cannot tell whether that is two milliseconds or
 * two hundred. The lines are drawn in the same grid as the rows so they line
 * up exactly, and they stay behind the bars because they are scaffolding.
 */
.twfruler { height: 0; }
.twfruler .twfbar { background: none; height: 0; overflow: visible; }
.twftickline { position: absolute; top: 0; width: 1px; height: 1000px; background: var(--line); opacity: .5; }
.twfaxis { height: 14px; margin-bottom: 2px; }
.twfaxis .twfbar { background: none; height: 14px; overflow: visible; }
.twftlabel { position: absolute; top: 0; transform: translateX(-50%); font-size: 10px; color: var(--fg-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The first and last labels would hang off the ends of the track. */
.twftlabel:first-child { transform: none; }
.twftlabel:last-child { transform: translateX(-100%); }

/*
 * Alternating rows. A twenty-row waterfall with connectors is a lot of thin
 * lines; the banding is what keeps a name and its bar on the same row when
 * the eye travels sideways.
 */
.twf .twfrow:not(.twfruler):not(.twfaxis):nth-of-type(even) { background: color-mix(in srgb, var(--fg) 3%, transparent); }
.twf .twfrow:not(.twfruler):not(.twfaxis) { padding: 1px 4px; margin: 0 -4px; border-radius: 3px; }
.twf .twfrow:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }

/*
 * A log line and an exception are moments, not durations.
 *
 * A diamond on the timeline at the instant it happened. Giving them a bar of
 * some width would say they took time, and the first question about an
 * exception is when it happened relative to everything else.
 */
.twfbar > b.mark {
  position: absolute; top: 3px; width: 8px; height: 8px;
  margin-left: -4px; border-radius: 2px; transform: rotate(45deg);
  background: var(--fg-3);
}
.twfbar > b.mark.logerr, .twfbar > b.mark.exception { background: var(--bad); }
.twfrow.twfmark { min-height: 16px; }
.twfmark .marktext { color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; }

/*
 * The stack, under the exception that raised it.
 *
 * An exception without its stack is a rumour: the message says what went
 * wrong and nothing about where, which is the half anybody opens the trace
 * for. Indented to the row it belongs to, scrollable rather than wrapped,
 * because a stack frame is a line and folding it makes it unreadable.
 */
.twfstack { margin: 0 0 6px 26px; }
.twfstack pre {
  margin: 0; padding: 6px 8px;
  font-family: var(--mono); font-size: 10.5px; line-height: 1.55;
  color: var(--fg-2); background: var(--panel-2);
  border-left: 2px solid var(--bad); border-radius: 3px;
  overflow-x: auto; white-space: pre;
}
.twfmark.is-failed .marktext { color: var(--bad); }
/* The function the stack blames, beside the message it raised. */
.markwhere { color: var(--fg-3); font-size: 10px; white-space: nowrap; }
.twfname .k.log { background: var(--fg-3); }
.twfname .k.logerr, .twfname .k.exception { background: var(--bad); }

/*
 * A hop's own time, as a band under its bar.
 *
 * "This service spent its three seconds waiting for the next one" and "it
 * spent them on the processor" are the same row until something says which.
 * Thin and low-contrast on purpose: it explains the bar above it rather than
 * competing with it.
 */
.hopband { position: absolute; bottom: 0; height: 3px; display: flex; border-radius: 2px; overflow: hidden; }
.hopband > u { display: block; height: 100%; text-decoration: none; }
.hopband > u.b-cpu { background: var(--s1); }
.hopband > u.b-gc { background: var(--warn); }
.hopband > u.b-loop { background: var(--bad); }
.hopband > u.b-wait { background: var(--fg-3); opacity: .3; }

/*
 * The window a row occupied, under how long it took.
 *
 * A duration says how long; only a start and an end say where. Two forty
 * millisecond calls are the same number whether they ran one after the other
 * or at the same time, and in a chain of services that is the finding.
 */
.twfat { display: block; font-size: 10px; color: var(--fg-3); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ------------------------------------------------------ a trace page --- */

/*
 * The four figures somebody opened the page for.
 *
 * A fact grid gave every value the same 170 pixels whatever its length, so a
 * duration and a user-agent were the same size and neither was legible. These
 * are read from across a desk; the context under each one is a whole sentence
 * with room to be one.
 */
.tfig { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 8px 26px; margin-bottom: 16px; }
.tfig > div { min-width: 0; }
.tfig .k { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); }
.tfig > div > b { display: block; font-size: 21px; font-weight: 650; line-height: 1.25; color: var(--fg); overflow-wrap: anywhere; }
.tfig .sub { display: block; font-size: 12px; color: var(--fg-2); overflow-wrap: anywhere; }
.tfig .warnfg { color: var(--warn); }
.tfig .badfg { color: var(--bad); }

/*
 * Headers, as name and value rather than as cards.
 *
 * Two columns on a wide screen — what was asked, what came back — and one on a
 * narrow one. The value keeps the room it needs and wraps; truncating it would
 * hide exactly the tail of a content-type or a vary that anybody is reading it
 * for.
 */
.hgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 4px 28px; }
.hcap { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); margin: 0 0 4px; }
.hrow { display: grid; grid-template-columns: minmax(96px, 150px) 1fr; gap: 12px; padding: 4px 0; border-top: 1px solid var(--line); font-size: 12px; }
.hrow:first-of-type { border-top: 0; }
.hname { color: var(--fg-2); overflow-wrap: anywhere; }
.hval { color: var(--fg); overflow-wrap: anywhere; font-size: 11px; line-height: 1.5; }

/* ------------------------------------------------- execution breakdown --- */

/*
 * One bar, four parts, and only the first three are measurements.
 *
 * Three categorical slots and a neutral: waiting is the absence of work, not a
 * fourth kind of it, so it takes the surface tone rather than a hue that would
 * make it look like another thing the request was doing.
 */
.xbar { display: flex; height: 10px; border-radius: 3px; overflow: hidden; background: var(--panel-2); margin: 2px 0 10px; }
.xbar > i { display: block; height: 100%; }
.xbar > i + i { box-shadow: inset 2px 0 0 var(--panel); }
.xbar > i.cpu { background: var(--s1); }
.xbar > i.db { background: var(--s3); }
.xbar > i.http { background: var(--s2); }
.xbar > i.wait { background: var(--fg-3); opacity: .35; }
.xlegend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 12px; color: var(--fg-2); margin-bottom: 16px; }
.xlegend > span { display: inline-flex; align-items: center; gap: 6px; }
.xlegend i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.xlegend i.cpu { background: var(--s1); }
.xlegend i.db { background: var(--s3); }
.xlegend i.http { background: var(--s2); }
.xlegend i.wait { background: var(--fg-3); opacity: .35; }

/* ------------------------------------------------------------- logs --- */

/*
 * A log line is monospace and it is long. It gets the width the table can
 * spare and stops at one line: a table where one row is forty lines tall is
 * not a table, and the whole message is on hover and in the row itself.
 */
.logmsg {
  font-family: var(--mono); font-size: 11px; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 100%;
}

.twfrow.is-current .twfname { color: var(--fg); font-weight: 600; }
.twfrow.is-current { border-left: 2px solid var(--accent); margin-left: -8px; padding-left: 6px; }
/* The service a hop belongs to, recessive except on the current row. */
.twfname .badge { font-size: 10px; padding: 0 4px; vertical-align: 1px; }
.twfname .badge.muted { background: transparent; color: var(--fg-3); border-color: var(--line); }

/* A two-state toggle inside a card heading: which list is being shown. */
.btn.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }

/* ---------------------------------------------------- table filter/limit --- */

/*
 * Ten rows and a search box.
 *
 * The box only appears when there is more than a screenful to filter, so short
 * tables stay short. The count beside it says how many matched out of how
 * many — a table showing ten of a hundred and thirty rows in silence is worse
 * than a long one.
 */
.tablefilter { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.tablefilter input {
  flex: 1; min-width: 0; max-width: 320px;
  padding: 5px 10px; font-size: 12.5px;
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--panel-2); color: var(--fg);
}
.tablefilter input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
.tablefoot { padding: 8px 2px 0; }
.tablefoot a { font-weight: 600; }

/* ------------------------------------------------------------- a switch --- */

/*
 * On or off, in a table cell.
 *
 * A real button, not a checkbox styled to look like one: it opens a
 * confirmation and starts a restart on a server, which is a command rather
 * than a form field. aria-pressed carries the state for anything not looking
 * at the colour, and the title says what it will do to which unit.
 */
.switch {
  position: relative; width: 34px; height: 19px; padding: 0;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel-2); cursor: pointer;
  transition: background .15s, border-color .15s;
  vertical-align: middle;
}
.switch > i {
  position: absolute; top: 2px; left: 2px;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--fg-3);
  transition: transform .15s, background .15s;
}
.switch.on { background: color-mix(in srgb, var(--good) 22%, transparent); border-color: var(--good); }
.switch.on > i { transform: translateX(15px); background: var(--good); }
.switch:hover { border-color: var(--accent); }
.switch:disabled { opacity: .5; cursor: default; }
.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/*
 * Depth in the trace tree.
 *
 * A tick and an indent, not a box: the nesting here is at most two or three
 * deep and a full tree drawing would be more furniture than information. The
 * kind dot carries what sort of work it was — database, cache, http — so the
 * eye can find the queries without reading every name.
 */
.twfname { display: flex; align-items: center; gap: 6px; }
.twftick { width: 8px; height: 8px; border-left: 1px solid var(--line); border-bottom: 1px solid var(--line); margin-right: 2px; flex: none; }
.twfname .k { width: 7px; height: 7px; border-radius: 2px; flex: none; background: var(--fg-3); }
.twfname .k.self { background: var(--s1); }
.twfname .k.db { background: var(--s3); }
.twfname .k.cache { background: var(--warn); }
.twfname .k.http, .twfname .k.fetch { background: var(--s2); }
.twfbar > i.db { background: var(--s3); }
.twfbar > i.cache { background: var(--warn); }
.twfbar > i.fetch { background: var(--s2); }

/*
 * A count inside an active chip.
 *
 * `.muted` is a grey chosen against the page, and on the filled accent
 * background it disappears entirely — the number was there and unreadable.
 * Inside a selected chip it borrows the chip's own ink instead.
 */
.chip.on .muted { color: var(--accent-ink); opacity: .72; }

/* Sampled frames: a different kind of statement, so a different colour. */
.twfname .k.code { background: var(--s3); }
/*
 * A sampled row draws twice: the pale bar is the whole branch, the solid one
 * is the part that was on top of the stack. Seven nested rows of equal length
 * say nothing about which of them is doing the work.
 */
.twfbar > i.code { background: var(--s3); opacity: .85; }
.twfbar > i.code.branch { opacity: .28; }

/* ---------------------------------------------------------- problems ---- */

/*
 * A third status step. Everything above "warning" used to be the same red,
 * which is the fastest way to teach a reader that red means nothing.
 */
.badge.serious { background: var(--serious-bg); color: var(--serious);
  border-color: color-mix(in srgb, var(--serious) 30%, transparent); }
.kpi .value.serious { color: var(--serious); }

/*
 * No margin of its own: the two pages that are not stacked pass their own,
 * and inside a stack this landed on top of the gap — 28px under the filter
 * bar and 14 everywhere else, which reads as an accident because it was.
 */
.pbar { padding: 10px 14px; }
/*
 * On a phone the filter bar wrapped to three rows — the segmented control, the
 * sentence about when the detectors last ran, and the button — which put the
 * problem list 589px down an 844px screen.
 *
 * The sentence is the part that can wait: it is context, not a control, so it
 * drops to its own line under the two things you might press and loses the
 * detector's schedule, which is the same on every install and is written out
 * in full in the empty state anyway.
 */
@media (max-width: 800px) {
  .pbar { padding: 8px 10px; }
  .pbar .row { gap: 8px !important; }
  .pbar .spacer { display: none; }
  .pbar > .row > .muted.small {
    order: 9; flex-basis: 100%; font-size: 11px; margin-top: 2px;
  }
}
.pbar .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
.pbar .seg a { display: inline-block; text-decoration: none; }
.pbar .seg a,
.pbar .seg button { background: var(--panel); border: 0; border-right: 1px solid var(--line);
  color: var(--fg-2); font: inherit; font-size: 13px; padding: 5px 12px; cursor: pointer; }
.pbar .seg a:last-child,
.pbar .seg button:last-child { border-right: 0; }
.pbar .seg a.on,
.pbar .seg button.on { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
.pbar .chip { background: var(--panel-2); border: 1px solid var(--line); border-radius: 20px;
  color: var(--fg-2); font: inherit; font-size: 12px; padding: 3px 10px; cursor: pointer; }

/*
 * The empty state, which is the one most people see. It is a statement that
 * the engine looked and found nothing, so it is deliberately not styled like
 * an error or like a page that failed to load.
 */
.pempty { text-align: center; padding: 34px 24px; }
.pempty h3 { display: block; margin: 0 0 8px; font-size: 17px; }
.pempty p { max-width: 62ch; margin: 0 auto 6px; line-height: 1.55; }
.pemptyicon { width: 42px; height: 42px; margin: 0 auto 14px; border-radius: 50%;
  background: var(--good-bg); color: var(--good); font-size: 21px; line-height: 42px; }

.ptitle a { font-weight: 600; }
.ptitle .small { margin-top: 2px; }

/* ---- one ticket ---- */

/*
 * A coloured left edge rather than a coloured card: the severity has to be
 * legible at a glance without the whole ticket becoming a warning box that the
 * eye then has to read through.
 */
.phead { border-left: 3px solid var(--line); }
.phead.bad { border-left-color: var(--bad); }
.phead.serious { border-left-color: var(--serious); }
.phead.warn { border-left-color: var(--warn); }
.phead h2 { margin: 0; font-size: 19px; line-height: 1.35; }
.pmeta { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 10px;
  color: var(--fg-3); font-size: 13px; }
.pmeta b { color: var(--fg); font-weight: 600; }

.psummary { margin: 0; max-width: 78ch; line-height: 1.6; color: var(--fg); }
/* The model's paragraph is set apart from the measured one on purpose. */
.paicard { border-left: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); }

/* The measurement as a figure: normal, then now, on one scale. */
.pfig { margin-top: 14px; max-width: 520px; }
.pbrow { display: grid; grid-template-columns: 72px 1fr 88px; align-items: center;
  gap: 10px; margin-bottom: 6px; }
.pblabel { color: var(--fg-3); font-size: 12px; }
.pbtrack { display: block; background: var(--panel-2); border-radius: 3px; height: 10px;
  overflow: hidden; }
.pbfill { display: block; height: 100%; border-radius: 0 3px 3px 0; }
.pbfill.base { background: var(--fg-3); opacity: .45; }
.pbfill.now { background: var(--bad); }
.pbval { text-align: right; font-variant-numeric: tabular-nums; font-size: 13px; }

.pcauses { list-style: none; margin: 0; padding: 0; }
.pcause { display: grid; grid-template-columns: 132px 1fr; gap: 14px;
  padding: 12px 0; border-top: 1px solid var(--line); }
.pcause:first-child { border-top: 0; padding-top: 2px; }
.parea { color: var(--fg-3); font-size: 12px; text-transform: uppercase;
  letter-spacing: .04em; padding-top: 2px; }
.ptext { line-height: 1.6; max-width: 82ch; }
.ptraces { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.ptraces a { display: block; padding: 5px 9px; background: var(--panel-2); border-radius: 6px;
  font-size: 13px; }
.pdetail { margin: 8px 0 0; padding: 8px 10px; background: var(--panel-2); border-radius: 6px;
  font-size: 12px; overflow-x: auto; white-space: pre; }

.ptimeline { list-style: none; margin: 0; padding: 0; }
.pev { display: grid; grid-template-columns: 122px 84px 1fr; gap: 12px; align-items: baseline;
  padding: 9px 0; border-top: 1px solid var(--line); }
.pev:first-child { border-top: 0; }
.pevt { color: var(--fg-3); font-size: 12px; font-variant-numeric: tabular-nums; }
.pevx { line-height: 1.55; max-width: 82ch; }

@media (max-width: 700px) {
  .pcause, .pev { grid-template-columns: 1fr; gap: 4px; }
  .pbrow { grid-template-columns: 60px 1fr 72px; }
}

/* --------------------------------------------------- host display names ---- */

/*
 * A machine's name, editable where it is read.
 *
 * The pencil only appears on hover (and always on keyboard focus, which hover
 * cannot express): a row of pencils down a list of cards is noise, and the
 * name is what the reader came for.
 */
.hname { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; }
.hnametext { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hnameedit {
  background: none; border: 0; color: var(--fg-3); cursor: pointer; font: inherit;
  font-size: 12px; line-height: 1; padding: 2px 4px; border-radius: 4px;
  opacity: 0; transition: opacity .12s;
}
/*
 * The pencil appears on the whole card, not just on the name itself.
 * A four-word target that only reveals itself when the pointer is exactly on
 * it is a control most people never discover.
 */
.hostcard:hover .hnameedit,
.hname:hover .hnameedit,
.hnameedit:focus-visible { opacity: 1; }
.hnameedit:hover { background: var(--panel-2); color: var(--fg); }
.hnameform input {
  font: inherit; font-size: inherit; font-weight: inherit;
  padding: 1px 6px; width: 100%; max-width: 260px;
}
.hostcard .hrealname { margin-top: -2px; }

/* ------------------------------------------------------- topology map ----- */

/*
 * A layered dependency map. Direction is down: every line from a box to a box
 * below it is one measured call, and that constraint is what makes the picture
 * readable without an interactive layout engine.
 */
.scape { width: 100%; overflow-x: auto; }
.scape svg { min-width: 560px; }

.scape-band { stroke: var(--line); stroke-width: 1; stroke-dasharray: 2 5; opacity: .5; }
.scape-layer { fill: var(--fg-3); font-size: 10px; font-weight: 700; letter-spacing: .07em; }
.scape-hint { fill: var(--fg-3); font-size: 10px; opacity: .7; }

.scape-edge {
  fill: none; stroke: var(--fg-3); opacity: .38;
  transition: opacity .15s;
}
.scape-edge.bad { stroke: var(--bad); opacity: .65; }
.scape-edge.dim { opacity: .05; }
/* Structural, not traced: it runs there, but no call was ever followed to it. */
.scape-edge.loose { opacity: .16; stroke-dasharray: 3 4; }

.scape-node { cursor: pointer; }
.scape-box {
  fill: var(--panel-2); stroke: var(--line); stroke-width: 1;
  transition: stroke .15s, filter .15s;
}
.scape-rule { fill: var(--edge, var(--line)); }
.scape-node.h-warn .scape-rule { fill: var(--warn); }
.scape-node.h-bad  .scape-rule { fill: var(--bad); }
/* The application and the origins it is served from are yours; the rest is not. */
.scape-node.l-app .scape-rule,
.scape-node.p-self .scape-rule { fill: var(--s1); }
.scape-node.l-service .scape-rule { fill: var(--s2); }
.scape-node.l-host .scape-rule { fill: var(--s3); }
/* Somebody else's computer, reached by our code rather than by the browser. */
.scape-node.l-external .scape-rule { fill: var(--c-domain); }

.scape-name { fill: var(--fg); font-size: 12.5px; font-weight: 600; }
.scape-sub  { fill: var(--fg-3); font-size: 10.5px; }
.scape-node.l-app .scape-name { font-size: 13.5px; }

.scape-node:hover .scape-box,
.scape-node:focus-visible .scape-box { stroke: var(--s1); }
.scape-node:focus-visible { outline: none; }
.scape-node.picked .scape-box { stroke: var(--s1); stroke-width: 2; }
/*
 * Dimming, not hiding. A map that removes the unselected parts loses the sense
 * of how much there is; one that fades them keeps the scale visible while the
 * path stands out.
 */
.scape-node.dim { opacity: .16; }
.scape-node.unrelated .scape-box { stroke-dasharray: 3 3; }
.scape-node.unrelated .scape-name { fill: var(--fg-3); }

.scape-sel {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); min-height: 24px;
}
.chip {
  display: inline-flex; gap: 6px; align-items: center;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 20px;
  padding: 4px 12px; font-size: 12.5px;
}

/* ------------------------------------------------------- log sources ------ */

/*
 * Choosing which logs to collect, grouped so the answer is near the top.
 *
 * This was one table of forty rows with a filter over it, which put the file
 * somebody came for behind a search they had to know to type. The groups are
 * the recommendation: what is already on, what looks like your own software,
 * then the distribution's housekeeping — closed, because nobody comes here
 * for dpkg.log.
 */
.lgrow {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 10px; border-radius: 8px; border: 1px solid transparent;
}
.lgrow + .lgrow { border-top: 1px solid var(--line); border-radius: 0; }
.lgrow.on { background: var(--panel-2); border-color: var(--line); border-radius: 8px; }
.lgrow.on + .lgrow { border-top: 0; }
.lgname { min-width: 0; flex: 1; }
.lgname .mono { font-size: 12.5px; }

.lgrec {
  border: 1px solid color-mix(in srgb, var(--good) 35%, transparent);
  background: var(--good-bg);
  border-radius: 10px; padding: 12px; margin-bottom: 14px;
}
.lgrec .lgrow.on, .lgrec .lgrow { background: transparent; border-color: transparent; }
.lgrec .lgrow + .lgrow { border-top: 1px solid color-mix(in srgb, var(--good) 22%, transparent); }

.lggroup { border-top: 1px solid var(--line); }
.lggroup > summary {
  cursor: pointer; padding: 10px 2px; display: flex; gap: 8px; align-items: baseline;
  list-style: none;
}
.lggroup > summary::-webkit-details-marker { display: none; }
/* A disclosure triangle that turns, so the state is visible without reading. */
.lggroup > summary::before {
  content: '▸'; color: var(--fg-3); font-size: 11px; transition: transform .12s;
  display: inline-block; width: 12px;
}
.lggroup[open] > summary::before { transform: rotate(90deg); }
.lggroup > summary:hover { color: var(--fg); }
.lggroup .lgon { margin-left: auto; color: var(--good); }

/*
 * The pen on the map: what is running beside the traced system and reaches it
 * nowhere. Dashed, because that is the same language the unattached machines
 * already speak, and captioned, because a box with no explanation is a puzzle.
 */
.scape-pen {
  fill: var(--panel-2); fill-opacity: .5;
  stroke: var(--line-2); stroke-width: 1; stroke-dasharray: 4 4;
}
.scape-pencap {
  fill: var(--fg-3); font-size: 10px; font-weight: 600; letter-spacing: .03em;
}

/* ------------------------------------------------- topology: network ------ */

/*
 * The same map as a free-form network. Things that talk end up near each other,
 * hubs look like hubs, and anything nothing connects drifts to the edge — the
 * separation the layered view has to draw a pen for happens here by itself.
 */
/*
 * A surface of its own, with an edge.
 *
 * The map sat on the card's own colour, so it had no boundary and the nodes
 * had nothing to sit against — the whole picture read as faint marks on the
 * page rather than as a thing to look at.
 */
.net {
  width: 100%; overflow: hidden; border-radius: 10px;
  background: var(--bg); border: 1px solid var(--line);
}
.net svg { cursor: grab; }
.net.panning svg { cursor: grabbing; }

.net-edge { stroke: var(--fg-3); opacity: .5; transition: opacity .15s, stroke .15s; }
.net-edge.bad { stroke: var(--bad); opacity: .6; }
.net-edge.loose { opacity: .12; stroke-dasharray: 3 4; }
.net-edge.dim { opacity: .04; }
/*
 * The lines of the thing you clicked are the answer to the question you asked,
 * so they are drawn up rather than merely left alone while the rest fades.
 */
.net-edge.strong { stroke: var(--s1); opacity: .95; stroke-width: 2.4px; }

.net-node { cursor: pointer; }
.net-halo {
  fill: var(--dot, var(--fg-3)); opacity: .11; pointer-events: none;
  transition: opacity .15s;
}
.net-node:hover .net-halo, .net-node.picked .net-halo { opacity: .26; }
.net-dot {
  fill: var(--dot, var(--fg-3)); fill-opacity: .95;
  stroke: var(--bg); stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  transition: fill-opacity .15s;
}
/* A process is a ring: the same colour as the service it runs, hollow. */
.net-dot.ring {
  fill: none; stroke: var(--dot, var(--fg-3)); stroke-width: 3.2;
}
.net-node.unrelated .net-dot.ring { stroke-opacity: .4; }
.net-node.unrelated .net-dot { fill-opacity: .3; stroke-dasharray: 2 2; }
.net-node:hover .net-dot, .net-node:focus-visible .net-dot { fill-opacity: 1; stroke: var(--fg); }
.net-node:focus-visible { outline: none; }
.net-node.picked .net-dot { stroke: var(--fg); stroke-width: 2.5; fill-opacity: 1; }
.net-node.hit .net-dot { stroke: var(--warn); stroke-width: 3; }
.net-node.dim { opacity: .12; }

/*
 * Labels are noise at a distance and the whole point close up, so they appear
 * with the zoom — and always for whatever is picked or matched by a search.
 */
/* Divided by the zoom, so a name is the same size on screen at every scale. */
.net-label {
  fill: var(--fg-2);
  font-size: calc(13px / var(--k, 1) * min(var(--k, 1), 2));
  opacity: 0;
  transition: opacity .15s;
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: calc(3px / var(--k, 1));
  stroke-linejoin: round;
}
.net.zoomed .net-label { opacity: 1; }
.net-node.picked .net-label, .net-node.hit .net-label { opacity: 1; fill: var(--fg); font-weight: 600; }

.topofind input[type="search"] { max-width: 340px; }
.seg.sm button { font-size: 12px; padding: 3px 10px; }
/* A search match on the layered map gets the same ring as on the network. */
.scape-node.hit .scape-box { stroke: var(--warn); stroke-width: 2; }
.scape-edge.strong { stroke: var(--s1); opacity: .9; }
/* The rest of the chain the selection belongs to: the same colour, quieter,
 * so "what leads here and where does it go" is one shape rather than one hop
 * and a lot of grey. */
.scape-edge.path { stroke: var(--s1); opacity: .45; }

/* The network legend: one swatch per kind of thing, matching the dots. */
.tp-legend .dot.n-app     { background: var(--c-app); border-radius: 3px; }
.tp-legend .dot.n-domain  { background: var(--c-domain); }
.tp-legend .dot.n-service { background: var(--c-service); }
/* Hollow, exactly as it is drawn on the map. */
.tp-legend .dot.n-process { background: transparent; box-shadow: inset 0 0 0 2px var(--c-service); }
.tp-legend .dot.n-host    { background: var(--c-host); border-radius: 3px; }
/* Same colour as a domain: both are somebody else's computer. */
.tp-legend .dot.n-external { background: var(--c-domain); }

/*
 * The layout switch.
 *
 * It used to be a small segmented control tucked between the card heading and
 * the legend, where it read as a label rather than as something you press.
 * Now it leads the controls row, at the size of a control, with a glyph that
 * shows what each layout does — stacked bars for layers, scattered dots for
 * the network.
 */
.viewswitch {
  display: inline-flex; border: 1px solid var(--line-2); border-radius: 8px;
  overflow: hidden; background: var(--panel);
}
.viewswitch button {
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; border: 0; border-right: 1px solid var(--line);
  color: var(--fg-2); cursor: pointer; font: inherit; font-size: 13px;
  padding: 7px 14px; font-weight: 500;
}
.viewswitch button:last-child { border-right: 0; }
.viewswitch button:hover { background: var(--panel-2); color: var(--fg); }
.viewswitch button.on {
  background: var(--accent); color: var(--accent-ink); font-weight: 600;
}
.viewswitch button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Two glyphs drawn in CSS: three stacked rules, and a scatter of dots. */
.vsicon { width: 13px; height: 11px; display: inline-block; position: relative; opacity: .85; }
.vs-layers::before, .vs-layers::after,
.vs-layers > i { content: ''; position: absolute; left: 0; right: 0; height: 2px;
  background: currentColor; border-radius: 1px; }
.vs-layers::before { top: 0; }
.vs-layers::after { bottom: 0; right: 4px; }
.vs-network::before, .vs-network::after {
  content: ''; position: absolute; border-radius: 50%; background: currentColor;
}
.vs-network::before { width: 5px; height: 5px; left: 0; top: 0; }
.vs-network::after { width: 4px; height: 4px; right: 0; bottom: 0; }

/*
 * Open and closed on one list.
 *
 * The default view shows both, so the two have to be told apart without
 * reading a column: a live problem is at full strength and a closed one has
 * receded. Only the ink is dimmed, never the row's own background — a striped
 * table with faded stripes is harder to read than either.
 *
 * Opacity is deliberately not used: it would fade the severity badge's border
 * against the surface and cost the badge its shape, which is the part that
 * survives for a colourblind reader.
 */
tbody tr.pdone td { color: var(--fg-3); }
/*
 * A closed ticket is still a link, and has to keep looking like one.
 *
 * Painting the title in body ink made it read as plain text — the row lost the
 * only affordance saying it could be opened, which is the opposite of the
 * intent: history should be quieter, not unreachable. So it keeps the link
 * colour and gives up only weight and a little saturation.
 */
tbody tr.pdone .ptitle a {
  /*
   * The closed title keeps the accent unchanged, and recedes by WEIGHT alone.
   *
   * Two measured facts forced this. Mixing toward --fg-3 moved the colour in
   * opposite directions in the two themes, because the muted ink is darker
   * than the accent in light and lighter in dark — one theme gained contrast
   * exactly as the other lost it. Mixing toward the surface recedes in both,
   * but a sweep from 100% down to 60% never cleared 4.5:1 in either theme:
   * the accent itself measures 4.42:1 light and 4.12:1 dark, so every mix
   * starts below the floor and only goes down.
   *
   * Fading a link that is already at the limit would make history unreadable
   * rather than quiet. Weight, the muted summary beneath it, and the desaturated
   * badge carry the distinction instead — none of which costs contrast.
   */
  font-weight: 400;
}

/*
 * A closed problem's badge, told apart by more than saturation.
 *
 * saturate(0.3) alone measured ΔE 6.7 against the open badge for a Warning in
 * light mode — the threshold for two colours reading as different is 15, and
 * the two states sat almost on top of each other. Saturation only moves the
 * hue's purity; it leaves lightness untouched, and lightness is the channel
 * the eye separates most readily.
 *
 * So the closed badge also loses its fill and its weight: it becomes an
 * outline on the card's own surface while the open one stays a filled pill.
 * That is a difference in shape as well as colour, which is what a colourblind
 * reader needs — the same rule the severity palette follows everywhere else.
 */
tbody tr.pdone .badge {
  filter: saturate(0.55) opacity(0.75);
  background: transparent;
  font-weight: 500;
}
tbody tr.pdone:hover td { color: var(--fg-2); }
tbody tr.pdone:hover .badge { filter: none; }
tbody tr.plive .ptitle a { font-weight: 600; }

/*
 * The security page's two attack cards, side by side.
 *
 * The scanning card holds a map and three columns of its own, so it takes two
 * thirds; the SSH card is a small chart and a short table and takes one. Below
 * 1400px they stack — two thirds of a 1200px screen is not enough for a world
 * map and a table beside it.
 */
.secrow { display: grid; gap: 14px; align-items: start; }
@media (min-width: 1400px) {
  .secrow { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}
.secrow > * { min-width: 0; }

/*
 * A table card stops at the width its columns need.
 *
 * `table { width: 100% }` makes every table fill whatever it is given, so on a
 * wide screen the free space is shared out between the columns and a ten-column
 * table of short values ends up as ten wide columns of whitespace. Measured at
 * 2560px: the Services table was given 2250px for 1030px of text — 1220px of
 * slack — and Processes 2250px for 1474px.
 *
 * `width: fit-content` on the card lets it shrink to its table, and the
 * `max-width: 100%` keeps that from becoming an overflow when the table really
 * is wider than the page (where .tablewrap's own scrolling takes over).
 *
 * Applied by class rather than to every card: a card holding a chart or a map
 * must still stretch, because those size to their container by design.
 */
.card.fitted { width: max-content; max-width: 100%; }

/*
 * The SSH card's map only when it can be read.
 *
 * That card is a third of a row, so on a 1500px screen its map measured 369px
 * wide — every country in the width of a phone, which tells the reader nothing
 * the country counts beside it do not tell better. Below the width where the
 * card itself gets room, the map is dropped and the chart and the accounts
 * carry the card.
 *
 * container queries would say this more precisely, but the card's width is a
 * fixed fraction of the page here, so the page width is an exact proxy.
 */
@media (max-width: 2299px) {
  #sshmap { display: none; }
}

/*
 * The category glyph on a problem card and on each of its rows.
 *
 * Sized and dimmed deliberately: it is a second-glance signal, not a heading.
 * At full weight beside a title it competes with the severity badge, which is
 * the thing that must be read first — the icon answers "what kind", and the
 * badge answers "how bad", and only one of those decides what happens next.
 */
.pcat {
  font-size: 15px;
  line-height: 1;
  opacity: 0.75;
  /* Emoji render at wildly different widths; a fixed box keeps the titles
     beside them aligned down the column. */
  display: inline-block;
  width: 1.25em;
  text-align: center;
}
.card h3 .pcat { font-size: 16px; opacity: 1; }

/*
 * A synthetic check's screenshots, laid out by their own shape.
 *
 * A portrait (mobile) capture gets a phone-width column and the runs table
 * fills the rest of the row — full-bleed it was a strip of page adrift in
 * three screens of dead card. Landscape keeps the wide card it needs.
 */
.synrow { display: flex; gap: 14px; align-items: flex-start; }
.synrow .synshot.portrait { flex: 0 0 300px; }
.synrow .card.fitted { flex: 1 1 auto; }
@media (max-width: 1100px) {
  .synrow { flex-wrap: wrap; }
  .synrow .synshot.portrait { flex-basis: 260px; }
}

/*
 * Menu favourites. The row is a flex pair — link grows, star stays — so the
 * link keeps its full hit area. The star idles at whisper volume and only
 * speaks on hover or once chosen: a column of twenty grey stars would be
 * louder than the menu itself. On touch there is no hover, so a chosen star
 * is always visible and the rest stay reachable at full row height.
 */
.navrow { display: flex; align-items: center; }
.navrow a { flex: 1; min-width: 0; }
.favstar {
  background: none; border: 0; cursor: pointer;
  color: var(--fg-3); font-size: 13px; line-height: 1;
  padding: 6px 7px 6px 4px; margin-right: -1px; border-radius: 6px;
  opacity: 0; transition: opacity .12s ease, color .12s ease;
}
.navrow:hover .favstar, .favstar:focus-visible { opacity: 1; }
.favstar.on { opacity: 1; color: var(--warn); }
.favstar:hover { color: var(--warn); background: var(--panel-2); }
@media (hover: none) { .favstar { opacity: .55; } .favstar.on { opacity: 1; } }

/*
 * Who measured a waterfall row, and what the application said about it.
 *
 * Both belong in the name cell because both qualify the name: the badge says
 * how far the number beside it can be trusted — an exact measurement, a
 * stack-sample estimate, or the application's own account — and the
 * annotation says what the call was working on. `motionflow.days=14` beside a
 * 141 ms span is the difference between "this was slow" and "this was slow
 * for fourteen days of input", and a value that only exists on hover is a
 * value nobody has.
 *
 * The badge never shrinks; the annotation takes what is left and ellipsizes,
 * so a long attribute list can never push the function name off the row.
 * Colours are the established muted text on the established raised surface —
 * the four sources are told apart by their words and their border, not by
 * four new colour pairs nobody has measured.
 */
.twfname .src {
  flex: none; font-size: 9px; letter-spacing: .03em;
  padding: 0 4px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--fg-3);
  cursor: help; white-space: nowrap; opacity: .75;
}
.twfname .src.s-otel, .twfname .src.s-otlp { background: var(--panel-2); border-color: var(--line-2); }
.twfname .src.s-profiler { border-style: dashed; }
/*
 * What technology the row IS, in front of the name.
 *
 * The kind used to be a 7px square and a colour key elsewhere on the page, so
 * "which of these is the Mongo call" was a question the trace could not answer
 * at a glance. The words and icons are the catalogue's own — MongoDB is
 * MongoDB here and on the Technologies page. Same pill as the source badge —
 * this page does not need a second visual language — but darker ink and in
 * front, because it identifies the row while the source qualifies it.
 */
.twfname .ktag {
  flex: none; font-size: 9px; letter-spacing: .03em;
  padding: 0 4px; border-radius: 999px;
  border: 1px solid var(--line-2); background: var(--panel-2); color: var(--fg-2);
  cursor: help; white-space: nowrap;
}
/*
 * The row somebody came for, arriving from a statement. Marked by its
 * background alone: the tag already says what the row is, and a second label
 * saying "the one you clicked" was one badge too many on a dense row.
 */
.twfrow.is-matched { background: var(--accent-bg); }
.twfrow.is-matched .twfname { color: var(--fg); font-weight: 600; }
/*
 * The annotation is the last thing that may take space and the first that
 * gives it back: it is capped at a third of the name cell and ellipsizes,
 * so a long attribute list can neither push the function name out nor grow
 * the column into the bars — which are what the page is actually for.
 */
.twfattr {
  flex: 0 1 auto; min-width: 0; max-width: 34%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--fg-3); font-size: 10px; opacity: .85;
}
/* On a narrow screen the name alone is already the whole column. */
@media (max-width: 900px) { .twfattr { display: none; } }

/*
 * The mark on a legend key that has an explanation behind it.
 *
 * The key used to reach for that by putting an <abbr> in the label, which the
 * legend escapes — so the tag printed itself onto the chart. The explanation
 * is a title on the key now, and this is the only thing that says one exists:
 * small, muted, and not a word, so it cannot be mistaken for part of the
 * series name.
 */
.legend .key .tq {
  font-size: 8px; font-weight: 700; color: var(--fg-3);
  margin-left: 3px; vertical-align: 3px; cursor: help;
}
.legend .key[title] { cursor: help; }

/*
 * A sampled frame in the single waterfall.
 *
 * It sits on the same ruler as the measured calls, so it must not read as one:
 * the row is dimmer, and its bar is the pale "branch" fill with a solid core
 * for time on top of the stack. The badge on the name says "sampled" in words
 * — this only has to make the difference visible at a glance, before anybody
 * reads a label.
 */
.twfrow.is-sampled .twfname { color: var(--fg-3); }
.twfrow.is-sampled .twfbar { background: transparent; }

/*
 * The small dependency picture on a service page and in a trace.
 *
 * Absolutely positioned boxes over an SVG of the same size, so the curves and
 * the boxes are drawn from one set of coordinates rather than two that can
 * disagree. It scrolls sideways inside its card instead of shrinking: a
 * service name squeezed to four characters is not a smaller diagram, it is a
 * diagram nobody can read.
 */
.flowmap { overflow-x: auto; padding: 2px 0 6px; }
.flowmap { overflow-x: auto; }
.fmwrap { position: relative; }
.fmwrap svg { position: absolute; inset: 0; color: var(--line-2); overflow: visible; }
.fmedge { fill: none; stroke: var(--line-2); stroke-width: 1.5; }
.fmedge.bad { stroke: var(--bad); }
/* Placement, not traffic: the one dashed line this picture draws. */
.fmedge.dim { stroke-dasharray: 3 4; opacity: .55; }
.fmedge-l { fill: var(--fg-3); font-size: 10px; font-family: var(--mono); }
.fmnode { position: absolute; }
.fmnode a { text-decoration: none; display: block; height: 100%; }
.fmbox {
  height: 100%; box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: 6px 10px; border-radius: 8px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.fmnode a:hover .fmbox { border-color: var(--accent); }
/* The one being looked at, marked the way the waterfall marks its own row. */
.fmbox.is-current { border-color: var(--accent); box-shadow: inset 2px 0 0 var(--accent); }
.fmbox.warn { border-color: var(--warn); }
.fmbox.bad { border-color: var(--bad); }
.fmname {
  font-size: 12px; font-weight: 600; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fmsub {
  font-size: 10px; color: var(--fg-3); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/*
 * The live window, marked in the picker.
 *
 * A dot that pulses only while "now" is the selected range — the one control
 * on the page whose data moves without being asked. It is the same signal the
 * problem badge uses, for the same reason: an animation that runs permanently
 * stops meaning anything within a day.
 */
.range button.live { display: inline-flex; align-items: center; gap: 5px; }
.range button.live::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: var(--fg-3); flex: none;
}
.range button.live.on::before { background: var(--accent); animation: livedot 2s ease-in-out infinite; }
@keyframes livedot { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .range button.live.on::before { animation: none; } }

/*
 * The map tooltip: label left, value right.
 *
 * The rows were a label with a fixed margin after it, so the number began
 * wherever the word happened to end — "Sessions1", "Load p75536 ms" — and no
 * two numbers lined up. A flex row with the value pushed to the far edge
 * gives them a column to sit in, which is the whole reason to read several
 * figures at once.
 */
.tooltip > div,
.leaflet-rum-tip > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 18px;
}
.tooltip .k,
.leaflet-rum-tip .k { margin-right: 0; flex: 0 1 auto; }
.tooltip b,
.leaflet-rum-tip b { flex: 0 0 auto; text-align: right; }

/*
 * The new-build notice.
 *
 * Accent-coloured because it is the one thing in the topbar that is about the
 * page rather than about the data, and it appears once and then only if
 * somebody ignores it. It is a button, not a banner: reloading throws away a
 * filter somebody set and a chart they were reading, so it waits to be asked.
 */
.uinew {
  border-color: var(--accent); color: var(--accent);
  background: var(--accent-bg);
}
.uinew:hover { background: var(--accent); color: var(--bg); }

/* ---- system health -------------------------------------------------------
 * A meter is a label, a bar, a number and a note in one line — the note is
 * what stops a percentage being read as an opinion.
 */
.hmeters { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.hrow { display: grid; grid-template-columns: 110px minmax(120px, 1fr) 110px 1fr;
  align-items: center; gap: 12px; }
.hlabel { color: var(--fg-2); font-size: 13px; font-weight: 600; }
.hnote { color: var(--fg-3); font-size: 12px; }
.hrow .pbfill.good { background: var(--good); }
.hrow .pbfill.warn { background: var(--warn); }
.hrow .pbfill.bad  { background: var(--bad); }
.hrow .pbfill      { background: var(--s2); }
.hlimit { display: grid; grid-template-columns: 220px auto 1fr; align-items: center;
  gap: 12px; padding: 10px 0; border-top: 1px solid var(--line); }
.hlimit:first-child { border-top: 0; }
.hlimit label { color: var(--fg-2); font-size: 13px; }
@media (max-width: 720px) {
  .hrow, .hlimit { grid-template-columns: 1fr; gap: 4px; }
}

/* A pinned function: what it is, what it has been costing, and the way out. */
.pinrow { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(160px, 2fr) auto;
  align-items: center; gap: 14px; padding: 10px 0; border-top: 1px solid var(--line); }
.pinrow:first-child { border-top: 0; padding-top: 0; }
.pinchart { min-width: 0; }
@media (max-width: 720px) { .pinrow { grid-template-columns: 1fr; gap: 6px; } }

/* ---- what the agent can do ----------------------------------------------
 * Two columns of claim-and-explanation. The icon is a hairline glyph, not a
 * picture: it marks the row so the eye can find its way back, and carries no
 * meaning the sentence beside it does not already carry.
 */
.caps { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: 16px 24px; margin-top: 18px; }
.cap { display: grid; grid-template-columns: 26px 1fr; gap: 12px; align-items: start; }
.cap b { display: block; font-size: 13.5px; margin-bottom: 3px; }
.cap p { margin: 0; color: var(--fg-3); font-size: 12.5px; line-height: 1.55; max-width: 60ch; }
.cap p b { display: inline; color: var(--fg-2); font-weight: 600; }
.capicon { width: 22px; height: 22px; fill: none; stroke: var(--s1); stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round; margin-top: 2px; }
.hero h2 { font-size: 19px; }

/* Capability lines: terse statements, not paragraphs. The marker is a hairline
 * dash rather than a bullet — a list of facts, not a list of features. */
.cap ul { margin: 0; padding: 0; list-style: none; }
.cap li { color: var(--fg-3); font-size: 12.5px; line-height: 1.5; padding-left: 12px;
  position: relative; margin-bottom: 2px; }
.cap li::before { content: ''; position: absolute; left: 0; top: 9px; width: 6px; height: 1px;
  background: var(--fg-3); opacity: .6; }
.cap li b { color: var(--fg-2); font-weight: 600; }

/* ---- tiles ---------------------------------------------------------------
 * One question per tile, filling the row at whatever width the window has.
 * The alternative was one card holding four meters, a paragraph, a table and
 * six inputs — every one of them a separate question, and none of them easy
 * to find inside the others.
 */
.htiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(210px, 100%), 1fr));
  gap: 12px; margin-top: 14px; }
.htile { background: var(--panel-2); border: 1px solid var(--line); border-radius: 9px;
  padding: 12px 14px; min-width: 0; }
.ht-k { color: var(--fg-3); font-size: 11.5px; text-transform: uppercase;
  letter-spacing: .04em; margin-bottom: 4px; }
.ht-v { font-size: 21px; font-weight: 600; line-height: 1.15; margin-bottom: 8px;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.htile .pbtrack { display: block; width: 100%; height: 6px; margin-bottom: 8px; }
.htile .pbfill { background: var(--s2); }
.htile .pbfill.good { background: var(--good); }
.htile .pbfill.warn { background: var(--warn); }
.htile .pbfill.bad  { background: var(--bad); }
.htile.good .ht-v { color: var(--good); }
.htile.warn .ht-v { color: var(--warn); }
.htile.bad  .ht-v { color: var(--bad); }
.ht-s { color: var(--fg-3); font-size: 12px; line-height: 1.45; }
.ht-w { color: var(--fg-3); font-size: 11.5px; line-height: 1.45; margin-top: 6px;
  padding-top: 6px; border-top: 1px solid var(--line); opacity: .85; }
.htile.lim .ht-in { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.htile.lim input { width: 92px; }
.htile.lim .ht-in span { color: var(--fg-3); font-size: 12px; }

/*
 * Two to a row on a phone.
 *
 * A tile whose content is a label, a number and a bar does not need 210px, and
 * at one per row six of them are a screen and a half of scrolling for six
 * numbers. The ones that carry a sentence keep the full width — hence the
 * modifier rather than a blanket rule.
 */
@media (max-width: 560px) {
  .htiles.compact { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .htiles.compact .htile { padding: 10px; }
  .htiles.compact .ht-v { font-size: 17px; }
  .htiles.compact .ht-k { font-size: 10.5px; }
  .htiles.compact .ht-s { font-size: 11px; }
}

/* Two pictures of the same system, side by side where there is room. Below the
 * breakpoint the view switch comes back and only one is drawn. */
/*
 * The layers scroll; the network stays.
 *
 * The two are not the same kind of thing. The left column is a list that grows
 * with the install and is read top to bottom; the right one is a picture, and
 * a picture that scrolls off the screen while somebody reads the list beside
 * it is a picture they have to keep scrolling back to. Stretching it to the
 * list's height was worse still: a graph three times taller than its contents,
 * mostly empty.
 *
 * align-items: start so the card is its own height rather than the row's, and
 * sticky so it holds its place under the topbar while the list moves past.
 */
.topopair { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 14px; align-items: start; }
@media (min-width: 1240px) {
  .topopair > :last-child {
    position: sticky;
    /* Clear of the sticky topbar, which is 56px plus the page's own padding. */
    top: 72px;
    max-height: calc(100vh - 88px);
  }
}
.topopair > * { display: flex; flex-direction: column; min-width: 0; }
.topopair > * > div:last-child { flex: 1 1 auto; min-height: 0; }
.topopair { margin-bottom: 14px; }
.topolabel { color: var(--fg-2); font-size: 12px; font-weight: 600; margin-bottom: 6px; }
.topolabel .muted { font-weight: 400; }
@media (max-width: 1239px) { .topopair { grid-template-columns: minmax(0, 1fr); } }

/* The layered map keeps its own width; a narrow screen scrolls it. */
.scape { overflow-x: auto; }

/*
 * The technology tiles.
 *
 * An icon big enough to be recognised without reading, the name under it, and
 * the "where" small: the tile is scanned, not read. Auto-fit rather than a
 * fixed column count so the same card works on a phone and on a wide screen
 * without a breakpoint deciding for it.
 */
.techgrid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
}
.techtile {
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--panel-2); text-decoration: none; color: inherit;
}
.techtile:hover { border-color: var(--line-2); background: var(--panel); }
.techicon { font-size: 26px; line-height: 1.1; }
.techicon.sm { font-size: 15px; }
.techname { font-weight: 650; font-size: 13px; }
.techver { font-size: 11px; color: var(--fg-3); }
.techwhere { font-size: 11px; color: var(--fg-3); }

/*
 * The console.
 *
 * A terminal wants to be big and to have a dark ground whatever the rest of
 * the page is doing: a shell that follows the light theme is a shell nobody
 * recognises, and every program that draws in it assumes a dark background.
 */
/*
 * The console, on the page.
 *
 * A terminal wants room and a dark ground whatever the rest of the page is
 * doing: every program that draws in one assumes a dark background, and a
 * shell that follows the light theme is a shell nobody recognises.
 */
.conrow {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 12px; padding: 9px 0;
  border-top: 1px solid var(--line);
}
.conrow:first-child { border-top: 0; padding-top: 2px; }
.conname { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; min-width: 0; }
.termcard h3 { flex-wrap: wrap; }
.termbody {
  height: min(62vh, 620px); background: var(--void); border-radius: 8px;
  padding: 8px; overflow: hidden;
}
/*
 * Only the outer element is stretched. Forcing the screen and the viewport to
 * 100% overrode the emulator´s own arithmetic — it sizes those from the rows
 * it decided on — and the last row ended up 13px under the edge, cut in half.
 */
.termbody .xterm { height: 100%; }
@media (max-width: 720px) {
  .conrow { grid-template-columns: minmax(0, 1fr) auto; }
  .conrow > .btn { grid-column: 2; }
  .termbody { height: 60vh; }
}

/*
 * One instrumentable library per row: the switch, the thing, what it is doing.
 *
 * Not the pin row: that one is sized for a label, a chart and a button, and
 * borrowing it gave the checkbox a 220px column to itself while the name
 * wrapped into four lines next to an empty middle. Here the name takes the
 * space and the state stays on the right where the eye can run down it.
 */
.instrow {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 12px; padding: 10px 0;
  border-top: 1px solid var(--line); cursor: pointer;
}
.instrow:first-child { border-top: 0; padding-top: 2px; }
.instrow:hover { background: var(--panel-2); }
.instname { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; min-width: 0; }
.instname b { font-size: 13px; }
.instwhere { flex-basis: 100%; }
.inststate { text-align: right; white-space: nowrap; }
@media (max-width: 720px) {
  .instrow { grid-template-columns: auto minmax(0, 1fr); }
  .inststate { grid-column: 2; text-align: left; white-space: normal; }
}

/* A session under the machine it belongs to: indented, quieter, same grid. */
.conrow.consub { padding-left: 18px; border-top-style: dashed; }

/* The page list in the UI settings reuses the instrument rows' shape. */
.instlist { display: block; }

/*
 * A quick command is a plain button. The × that used to sit on it moved to
 * the section at the bottom of the page: "delete this" a few pixels from
 * "run this on a production machine" is a trap, not a convenience.
 */
.favhost { color: var(--s1); }

/*
 * The navigation, folded away.
 *
 * A column that is useful most of the time and in the way when the thing being
 * read is wide — a waterfall, a topology, a terminal. Folded it takes no space
 * at all rather than shrinking to a strip of icons: a strip of icons is a
 * second navigation to learn, and this one has named groups for a reason.
 *
 * The button that brings it back is the ☰ in the topbar, which is otherwise
 * the phone drawer's button — one control, and the layout decides what it
 * means.
 */
.nav-fold { display: none; }
/* Big enough to hit, quiet enough to ignore until it is wanted. */
.nav-fold { padding: 4px 6px; border-color: transparent; color: var(--fg-3); }
.nav-fold svg { width: 17px; height: 17px; display: block; }
.nav-fold:hover { border-color: var(--line-2); color: var(--fg); }
@media (min-width: 801px) {
  .nav-fold { display: inline-flex; margin-left: auto; }
  body.nav-folded .sidebar { display: none; }
  body.nav-folded .menu-btn { display: inline-flex; }
}

/*
 * The mode switch lives on the globe too: "names / every value" and
 * "request / response" answer the same question — which of these labels do I
 * want to see — and a card apart meant looking in two places to change one
 * picture. Neutral, because unlike the three directions they are not a
 * colour: they are two readings of the same set.
 */
.ckey.mode { color: var(--void-fg); text-decoration: none; }
.ckey.mode::before { content: none; }
.cloudsep {
  width: 1px; align-self: stretch; margin: 0 2px;
  background: var(--void-line);
}

/* A fact and its value, one per line: the mail sender, and what it did last. */
.kvrow {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  padding: 5px 0; border-top: 1px solid var(--line);
}
.kvrow:first-of-type { border-top: 0; }
.kvrow > .k { font-size: 10.5px; color: var(--fg-3); min-width: 96px; }
