@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; }

/*
 * hidden means hidden.
 *
 * The browser's own rule is [hidden] { display: none }, and any author rule
 * that sets `display` with a class in it beats it. It went wrong one element
 * at a time: the assistant panel stayed in the corner after its ✕, the
 * Administrator tick left the entity lists on screen (reading as "these ticks
 * still matter"), a .row of filters ignored it — and each got its own
 * `.x[hidden]` until there were fifteen. One rule, for every element:
 * nothing in this product sets `hidden` on something it means to show.
 */
[hidden] { display: none !important; }

/* ------------------------------------------------------------- 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);
}

/*
 * 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, .tbctl select.scope {
  width: auto; max-width: 260px; padding: 6px 10px; font-size: 13px;
  background: var(--panel-2); border-color: var(--line-2);
}
/* The page's controls: part of the bar on a desktop, a row under it on a phone (app.js render). */
.tbctl { display: contents; }
.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; }
/* The assistant's mark in the bar: a phone's (see the 800px block); the corner one serves a desktop. */
.topbar .tb-ai { display: none; color: var(--accent); }

.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; }
.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; }

/*
 * The two network cards on a host page, side by side.
 *
 * Not `grid g2`: the open-ports list is two columns wide and the interface
 * table is five, so an even split gives the short one room it cannot use and
 * squeezes the wide one into wrapping. Narrow first, and both min-width:0 so a
 * table that wants more room shrinks instead of pushing the page sideways.
 */
.netpair {
  display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 2fr);
  gap: 14px; align-items: start;
}
@media (max-width: 1000px) { .netpair { grid-template-columns: 1fr; } }
.netpair > .card { min-width: 0; }
/*
 * The host page (hostView). Six tiles: one row where they fit, three and
 * three below that, two a row on a phone (the .g6 rule). Its chart groups
 * are two columns, and an odd last card takes the whole row rather than
 * leaving half of it empty beside a taller neighbour.
 */
@media (min-width: 801px) {
  .grid.g6.hostkpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1400px) {
  .grid.g6.hostkpis { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (min-width: 1240px) {
  .grid.cards.hostsec { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.grid.cards.hostsec > :last-child:nth-child(odd) { grid-column: 1 / -1; }
.hoststrip { row-gap: 6px; }
/* On a phone the host's gear stays in the first row, beside the theme and account. */
@media (max-width: 800px) { .topbar .hostgear { order: 2; } }
.card h3.hsub { margin-top: 18px; }
/* A host's settings: a switch is a box beside its sentence, not half the card. */
.hostset label.row { flex-wrap: nowrap; }
/* Its size and flex are the global checkbox rule's ("A box or a dot is its own size"). */
.hostset label.row > input[type=checkbox] { margin: 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)); }
/* Five numbers on one row where they fit (the server security KPIs). */
.g5 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr)); }
.g6 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr)); }
/*
 * A row of nothing but stat tiles keeps its four on one line on an ordinary
 * desktop (asked for on 2026-10-27, Hosts, Processes, Services, Campaigns):
 * at 250px a tile, a 1000px content area fitted three and dropped the fourth
 * onto a row of its own. A tile is a label, a number and a line; 180px holds
 * it. Only rows made entirely of tiles — a row with a chart in it keeps its
 * own minimum — and at 0,1,0, so the host page's own arrangements still win.
 */
@media (min-width: 801px) {
  .grid:where(:has(> .kpi):not(:has(> :not(.kpi)))) { grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 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.g5:has(.kpi), .grid.g6:has(.kpi) {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }
}
@media (max-width: 380px) {
  .grid.g4:has(.kpi), .grid.g5: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; }
}
/*
 * The ticket page's flow, wider on wider screens: about 650px a column, which is
 * what its tables and its 90ch paragraphs need. Never more columns than cards —
 * two cards in a four-column flow leave two columns standing empty.
 */
@media (min-width: 2200px) {
  .pflow { column-count: 3; }
  .pflow:has(> :nth-child(2):last-child) { column-count: 2; }
}
@media (min-width: 3000px) {
  .pflow { column-count: 4; }
  .pflow:has(> :nth-child(3):last-child) { column-count: 3; }
}

/*
 * 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;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A label with a ? keeps its popover (an overflow would clip it); it stays one line all the same. */
.kpi .label:has(.hintwrap) { overflow: visible; }
.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%;
}
/*
 * A box or a dot is its own size, everywhere. The rule above made every
 * checkbox 100% wide: in a flex label it took the row and pushed its words
 * into a 120px column at the far right (every modal form), and each
 * component grew its own fix. One rule, here, for all of them.
 */
input[type=checkbox], input[type=radio] {
  width: 15px; height: 15px; min-width: 15px; flex: none; padding: 0; accent-color: var(--accent);
}
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%; }

/*
 * ...except where the row's CONTROLS are the point.
 *
 * Sideways scrolling is a fair answer for a wide table of measurements: the
 * columns a reader wants on a phone are the first two, and the rest are there
 * for a desktop. It is the wrong answer when the last columns hold the
 * buttons. Measured on a 390px screen, the Accounts table is 619px wide inside
 * a 328px box: the "Sees" button starts 39px past the right edge, and the
 * password and remove buttons 129px past it. The page does not scroll (it must
 * not), so the only way to reach them is a swipe inside a narrow strip, and
 * what that looks like from the outside is "I cannot change anything on my
 * phone".
 *
 * So a table can opt in — `table(cols, rows, { stack: true })` — and becomes
 * one card per row below the phone breakpoint. Each cell keeps its column's
 * name in `data-label`, because a stacked row has no header above it to read
 * the value against.
 */
@media (max-width: 720px) {
  .tablewrap.stacked { overflow-x: visible; }
  .tablewrap.stacked table { width: 100%; min-width: 0; }
  .tablewrap.stacked thead { display: none; }
  .tablewrap.stacked tbody,
  .tablewrap.stacked tr,
  .tablewrap.stacked td { display: block; width: auto; }
  .tablewrap.stacked tr {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel-2);
    padding: 4px 12px;
    margin-bottom: 10px;
  }
  .tablewrap.stacked tbody tr:hover { background: var(--panel-2); }
  .tablewrap.stacked td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 7px 0;
    border-bottom: 1px solid var(--line);
    text-align: left;
  }
  .tablewrap.stacked td.num { text-align: left; }
  .tablewrap.stacked tr td:last-child { border-bottom: 0; }
  /* The column's name, on the left, in the same voice as the header it
     replaces. A cell whose column has no title — the actions — gets no label
     and takes the whole width for its buttons. */
  .tablewrap.stacked td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--fg-3);
    font-weight: 650;
  }
  .tablewrap.stacked td[data-label=""] { justify-content: flex-start; flex-wrap: wrap; gap: 8px; }
  .tablewrap.stacked td[data-label=""]::before { content: none; }
  /* The value, whatever it is made of, is ONE box on the right (table() wraps
     a stacked table's cells in .tdv): a date with its "ends in 3 days" line,
     a name with its badge, stay together instead of being pushed apart. A long
     value — an address — wraps rather than push the card wider than the screen. */
  .tablewrap.stacked td > .tdv { display: block; min-width: 0; flex: 0 1 auto; text-align: right; overflow-wrap: anywhere; }
  .tablewrap.stacked td[data-label=""] > .tdv { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 1 1 100%; text-align: left; }
  /* A column a row is not recognised by (prio 2 / 3, layout.md) stays hidden
     on a stacked phone card too: the stacked cell's display:flex above would
     otherwise beat the layout's display:none and bring it back. */
  .ly-page .tablewrap.stacked td.col-p2, .ly-page .tablewrap.stacked td.col-p3 { display: none; }
}
/* Not stacked (a wider screen): the value box is not there for layout. */
.tdv { display: contents; }

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; }
/* DB-IP's credit (CC BY 4.0): readable, but set apart from the legend's values. */
.geo-legend .geo-credit { margin-left: auto; font-size: 11px; color: var(--fg-2); }

.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); }
/* A no-break space: the arrow stays with the last word, never alone on a line. */
.kpi.linked .label::after { content: '\00a0→'; 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;
}

/* Server security — an overview of tiles, a page of detail behind each. */
.sshrdns { word-break: break-all; }

/* Section headings on the overview. */
.sechd { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 6px 0 10px; }
.sechd h2 { margin: 0; font-size: 15px; font-weight: 650; letter-spacing: -.01em; }

/* The tiles: one machine, application or door each, the whole tile a link. */
.stiles { display: grid; gap: 12px; margin-bottom: 22px;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 270px), 1fr)); }
.stile { display: flex; flex-direction: column; min-width: 0; padding: 14px 16px 12px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); color: inherit; text-decoration: none;
  transition: border-color .12s ease, transform .12s ease; }
.stile:hover { border-color: var(--accent); transform: translateY(-1px); text-decoration: none; }
.stile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.st-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
.st-title { font-weight: 650; font-size: 14px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-head .badge { flex: none; }
.st-sub { color: var(--fg-3); font-size: 12px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 14px; }
.st-stat { display: flex; flex-direction: column; min-width: 0; }
.st-stat b { font-size: 21px; font-weight: 650; line-height: 1.1; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.st-stat b.bad { color: var(--bad); }
.st-stat b.warn { color: var(--warn); }
.st-stat span { color: var(--fg-3); font-size: 11.5px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.st-spark { margin-top: 10px; }
.st-spark:empty { display: none; }
.st-spark svg { display: block; width: 100%; }
.st-foot { margin-top: auto; padding-top: 10px; border-top: 1px solid var(--line); color: var(--fg-3);
  font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-stats + .st-spark:empty + .st-foot, .st-stats + .st-foot { margin-top: 12px; }
.stile.dim { opacity: .62; }
/* The attack tiles are plain cards (asked for: the tinted card read as mud).
   What sets them apart is their data, drawn in red — the sparklines, the
   maps, the bars — not the card it sits on. */
/* The server security overview's top row: small tiles left, the to-do chart
   right, each half the width; one column on a narrow screen. */
.sectop { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; margin-bottom: 18px; align-items: stretch; }
.sectop > :only-child { grid-column: 1 / -1; }
.seckpis { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); align-content: start; }
.seckpis .kpi { padding: 10px 13px; }
.seckpis .kpi .value { font-size: 22px; margin-top: 2px; }
.seckpis .kpi .sub { font-size: 11.5px; }
.sectrend { min-width: 0; }
.sectrend h3 { margin-bottom: 6px; }
@media (max-width: 900px) { .sectop { grid-template-columns: minmax(0, 1fr); } }
/* A door map: refused in red, let in in green. */
.doormap { min-width: 0; }

/* The line under the overview: what is clean, and where the rest is. */
.secfoot { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; padding: 12px 0 4px;
  border-top: 1px solid var(--line); }

/* Detail pages: a way back, a title, and the actions on the right. */
.sechead { margin-bottom: 16px; }
.sh-back { font-size: 12.5px; }
.sh-row { display: flex; align-items: center; gap: 12px; margin-top: 6px; flex-wrap: wrap; }
.sh-main { min-width: 0; }
.sh-main h2 { margin: 0; font-size: 20px; font-weight: 650; letter-spacing: -.01em; }
.sh-sub { color: var(--fg-3); font-size: 13px; margin-top: 3px; }
/* As a page head (secHeader): the name at the page-title size, its sub-line beside it. */
.sechead.ly-head { margin-bottom: 0; }
.sechead.ly-head h2.ly-title { margin: 0; }
.sechead.ly-head .sh-back { font-size: 13px; }
.sechead.ly-head .sh-sub { margin-top: 0; font-size: 12.5px; }
/* A group heading on a page (Machines, Applications, Attacks) is the lySection size, not a second page title. */
.ly-page .sechd h2 { font-size: 13px; letter-spacing: 0; color: var(--fg); }
.ly-page .sechd h2 + .muted { font-size: 12px; }
.sh-right { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-wrap: wrap; }
.callout { background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; margin-bottom: 14px; }
/* The organisation's plan (plans.js): the banner when something is paused or full, the room left, the meter. */
.planbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between;
  background: var(--warn-bg); color: var(--fg); border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  border-radius: 8px; padding: 10px 14px; margin-bottom: 14px; }
.planbar > span { flex: 1 1 280px; min-width: 0; }
.planroom { margin: 6px 0 10px; }
.planmeter { height: 6px; border-radius: 3px; background: var(--panel-2); border: 1px solid var(--line); overflow: hidden; }
.planmeter > i { display: block; height: 100%; background: var(--accent); }
.planmeter.full > i { background: var(--warn); }
.planfull { color: var(--warn); font-weight: 600; }
/* Paused by the plan, in a list: greyed and locked; it leads to Plan & licence, not to its data. */
.card.planpaused { display: block; color: var(--fg-2); text-decoration: none; opacity: .62; filter: grayscale(1);
  border-style: dashed; }
.card.planpaused:hover, .card.planpaused:focus-visible { opacity: .85; }
.planpausedlist { list-style: none; margin: 10px 0 0; padding: 0; }
.planpausedlist li { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 6px 0; border-top: 1px solid var(--line); }
.kvrow { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-top: 1px solid var(--line); }
.kvrow:first-of-type { border-top: 0; }

/* A command with a copy button. */
.cmdbox { display: flex; gap: 8px; align-items: flex-start; margin: 2px 0 12px; }
.cmdbox .snippet { flex: 1; min-width: 0; padding: 8px 10px; }

/* The actions: a table whose rows open to their advisories. */
.atable { border-top: 1px solid var(--line); }
.ahead, .arow > summary { display: grid; gap: 12px; align-items: center; padding: 9px 6px;
  grid-template-columns: minmax(140px, 1.4fr) minmax(110px, 1fr) minmax(110px, 1fr) minmax(120px, auto) 92px; }
.ahead { color: var(--fg-3); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  border-bottom: 1px solid var(--line); }
.ahead .num, .arow .num { text-align: right; }
.arow { border-bottom: 1px solid var(--line); }
.arow > summary { cursor: pointer; list-style: none; }
.arow > summary::-webkit-details-marker { display: none; }
.arow > summary:hover { background: var(--panel-2); }
.arow > summary > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.a-caret { display: inline-block; margin-left: 6px; color: var(--fg-3); transition: transform .12s ease; }
.arow[open] .a-caret { transform: rotate(180deg); }
.adv { padding: 2px 6px 12px 18px; background: var(--panel-2); }
.adv-lead { color: var(--fg-2); font-size: 12.5px; padding: 8px 0 6px; }
.advrow { display: grid; grid-template-columns: 160px minmax(0, 1fr) auto auto; gap: 12px; align-items: center; padding: 5px 0; }
.ellip { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cardsum { cursor: pointer; list-style: none; }
.cardsum::-webkit-details-marker { display: none; }
.cardsum h3 { margin: 0; }
details.card[open] > .cardsum h3 { margin-bottom: 12px; }

/* Mute: a small popover beside the title, not a form across the page. */
.mutebox { position: relative; }
.mutebox > summary { list-style: none; }
.mutebox > summary::-webkit-details-marker { display: none; }
.mutepop { position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; width: min(300px, 86vw);
  display: grid; gap: 8px; padding: 12px; background: var(--panel); border: 1px solid var(--line-2);
  border-radius: 10px; box-shadow: var(--shadow-lg); }
.mutepop p { margin: 0; }

/* The attack tiles on a door's page. */
.atkgrid { display: grid; gap: 14px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); }
.atkgrid > * { min-width: 0; }
.atkgrid > .wide { grid-column: 1 / -1; }
/* Overview: the attack map beside the three door tiles. */
/* The map (with its list beside it) across the row, the three doors in a row
   under it. Side by side, the map's height is set by its width and never
   matched the doors' column: the tile was stretched and half empty. */
.atkover { display: grid; gap: 12px; margin-bottom: 22px; grid-template-columns: minmax(0, 1fr); }
.atkover > * { min-width: 0; }
.atkstack { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
/*
 * On a wide screen, all of it on one row — asked for by the operator: the map
 * with its countries, then the three doors. The stack dissolves into the row
 * (display: contents), the doors take the map's height, and their sparkline
 * grows into it rather than leaving a gap between the numbers and the line
 * under them. Below 1700px the map needs the width for its list, so the doors
 * go under it.
 */
@media (min-width: 1700px) {
  .atkover { grid-template-columns: minmax(0, 2.2fr) repeat(3, minmax(0, 1fr)); align-items: stretch; }
  .atkover .atkstack { display: contents; }
  .atkover .stile.atk .st-spark { flex: 1 1 auto; display: flex; align-items: flex-end; min-height: 34px; }
  .atkover .stile.atk .st-spark svg { height: 100% !important; min-height: 34px; }
  .atkover .stile.atk .st-spark .sparkwrap { align-self: stretch; }
}
.atkmaptile { container-type: inline-size; }
/* The facts about one application: where it runs, as what, and since when. */
.facts { display: grid; gap: 14px 28px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 190px), 1fr)); }
.fact { min-width: 0; }
.fact-k { color: var(--fg-3); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; }
.fact-v { font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; }
/* The map and the busiest countries side by side; stacked when the tile is narrow. */
.atkmapbody { display: grid; gap: 18px; align-items: start; grid-template-columns: minmax(0, 1fr) minmax(190px, 260px); }
.topc { display: grid; gap: 11px; align-content: start; padding-left: 18px; border-left: 1px solid var(--line); }
.topc-h { color: var(--fg-3); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
.topc-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 10px; align-items: baseline; font-size: 12.5px; }
.topc-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topc-n { text-align: right; font-variant-numeric: tabular-nums; color: var(--fg-2); }
.topc-bar { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
.topc-bar > span { display: block; height: 100%; background: var(--bad); border-radius: 2px; }
@container (max-width: 620px) {
  .atkmapbody { grid-template-columns: minmax(0, 1fr); }
  .topc { padding: 12px 0 0; border-left: 0; border-top: 1px solid var(--line); }
}
/* A tile's sentence is one sentence: the summary class keeps line breaks for tickets. */
.atknote { white-space: normal; }

@media (max-width: 700px) {
  .secpage .kpi { padding: 12px; }
  .secpage .kpi .value { font-size: 22px; }
  .ahead { display: none; }
  .arow > summary { display: flex; flex-wrap: wrap; gap: 4px 10px; }
  .arow > summary .a-pkg { flex: 1 1 100%; }
  .arow > summary .a-n { margin-left: auto; }
  .advrow { grid-template-columns: 1fr auto; }
  .advrow .ellip { grid-column: 1 / -1; white-space: normal; }
}

.modal-bg {
  position: fixed; inset: 0; background: var(--scrim);
  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; }
/* One dialog (modal() in app.js, 2026-10-29): a header with the title and a ✕, the form, and its action row
   pinned to the bottom while a long form scrolls. One button height (32 px, the settings one) and one field
   height in every dialog. */
.modal-h { display: flex; align-items: flex-start; gap: 12px; margin: 0 0 10px; }
.modal-h .modal-t { flex: 1; min-width: 0; }
.modal-h .modal-t h2 { margin: 0; }
.modal-h .modal-t h2 .hintwrap, .modal label .hintwrap { margin-left: 6px; }
.modal-x { flex: none; width: 32px; height: 32px; margin: -4px -6px 0 0; border: 1px solid transparent; border-radius: 8px;
  background: none; color: var(--fg-2); font-size: 15px; line-height: 1; cursor: pointer; }
.modal-x:hover { background: var(--panel-2); color: var(--fg); border-color: var(--line); }
.modal-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.modal .modal-f { position: sticky; bottom: -22px; z-index: 1; background: var(--panel); margin: 14px -22px -22px !important;
  padding: 12px 22px 18px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.modal .btn { padding: 7px 13px; font-size: 13px; line-height: 1.25; min-height: 32px; }
.modal .btn.sm { padding: 4px 10px; font-size: 12px; min-height: 0; }
.modal input:not([type=checkbox]):not([type=radio]):not([type=range]), .modal select { height: var(--field-h, 32px); box-sizing: border-box; padding-top: 0; padding-bottom: 0; }
.modal .seg > button { min-height: 0; }
/* Small buttons inside a dialog's body (+ Page, ✕) are one size: 28 px, the table-row size (round 5: 25 px). */
.modal .btn.sm { min-height: 28px; padding: 4px 10px; font-size: 12px; display: inline-flex; align-items: center; justify-content: center; }
/* A phone: a dialog's side-by-side fields stack (the policy's name, severity and Enabled were squeezed into one line). */
@media (max-width: 480px) {
  .modal form > .row.wrap > label, .modal form > .row.wrap > div { flex: 1 1 100% !important; width: auto !important; margin-top: 0 !important; }
}
/* The agent dialog: one line per change, the how-tos folded. */
.modal .amchanges { margin: 6px 0 12px; padding-left: 18px; line-height: 1.6; color: var(--fg-2); }
.modal .amchanges > li { margin: 2px 0; }
.modal details.amfold { margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--line); }
.modal details.amfold > summary { cursor: pointer; font-size: 13px; color: var(--fg-2); }
.modal details.amfold > p { margin: 8px 0 0; }
/* A long dialog folds its optional parts (the policy's scope and escalation). */
.modal details.policy-fold { margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--line); }
.modal details.policy-fold > summary { cursor: pointer; margin-bottom: 6px; }


.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; }
}

/* The tile server's credit must stay readable: its policy says "do not hide
   attribution", and over darkened tiles a faint grey at 10px effectively did. */
.leaflet-host .leaflet-control-attribution {
  background: color-mix(in srgb, var(--panel) 94%, transparent);
  color: var(--fg-2);
  font-size: 11px;
  padding: 2px 6px;
  border-radius: 5px 0 0 0;
}
.leaflet-host .leaflet-control-attribution a { color: var(--fg); }

.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%;
}

/*
 * A screenshot recording is a video, and the taps are drawn on a sheet over
 * it: the picture belongs to the application, the rings are ours, and putting
 * them in one canvas would mean re-drawing every video frame in JavaScript to
 * paint a circle on it.
 */
/*
 * Both fill the stage, which is sized once in JS and never moves. `fill`
 * rather than `contain`: the encoded picture and the surface can differ by
 * under a percent — the encoder rounds to whole macroblocks — and a letterbox
 * of a few pixels would put every tap ring that far off the thing it was
 * aimed at, which is worse than a stretch nobody can see.
 */
.replay-video {
  display: block; width: 100%; height: 100%;
  object-fit: fill; background: var(--panel-2);
}
.replay-stage.has-video .replay-wire {
  position: absolute; inset: 0; width: 100%; height: 100%;
}
/* The wireframe and the JPEG path draw at their own size; CSS scales them. */
.replay-stage:not(.has-video) .replay-wire { display: block; width: 100%; height: 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; }
  /* The title gives way (two lines at most, then an ellipsis) so the theme,
     assistant and account buttons stay on its row — a long ticket title pushed
     them to a third row. */
  .topbar h1 { flex: 1 1 0; min-width: 0; order: 1; font-size: 15px; line-height: 1.3;
    overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow-wrap: anywhere; }
  .topbar .theme-btn { order: 2; margin-left: auto; }
  .topbar .tb-ai { order: 2; display: inline-flex; }
  .topbar .usermenu { order: 3; }
  /* One row, and it is the one that stays: the page's controls are a row of
     their own under it (.tbctl), which scrolls away with the page. */
  .topbar .spacer { display: none; order: 4; }
  .topbar { flex-wrap: nowrap; }
  .tbctl { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 14px;
    border-bottom: 1px solid var(--line); background: var(--bg); }
  .tbctl select.scope { max-width: none; flex: 1 1 120px; }
  .tbctl .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;
  padding: 0 24px;
}

/* The first screen: the pitch beside the form, the form always in view. */
.landing-hero {
  position: relative;
  z-index: 1;
  min-height: min(100vh, 760px);
  min-height: min(100dvh, 760px);
  max-width: 1180px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 420px);
  gap: 56px;
  align-items: center;
  padding: 48px 0;
}
.landing-pitch h1 {
  font-size: clamp(38px, 5.4vw, 68px);
  line-height: 1.02;
  letter-spacing: -0.035em;
  margin: 14px 0 18px;
  font-weight: 800;
}
.landing-pitch h1 em {
  font-style: normal;
  color: var(--accent);
}
.landing-pitch p {
  font-size: clamp(16px, 1.5vw, 19px);
  line-height: 1.5;
  color: var(--fg-2);
  max-width: 34ch;
  margin: 0 0 28px;
}
.lf-kicker {
  display: inline-block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-bg);
  padding: 6px 10px;
  border-radius: 999px;
}
.landing-more {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  font-weight: 600;
  color: var(--fg);
  text-decoration: none;
  border-bottom: 2px solid var(--accent);
  padding-bottom: 2px;
}
.landing-more span { transition: transform .2s; }
.landing-more:hover span { transform: translateY(3px); }

/* ---- the feature section ---- */
.lf {
  position: relative;
  z-index: 1;
  max-width: 1180px;
  margin: 0 auto;
  padding: 24px 0 72px;
}
.lf-head { text-align: center; margin-bottom: 36px; }
.lf-head h2 {
  font-size: clamp(28px, 3.6vw, 46px);
  letter-spacing: -0.03em;
  line-height: 1.08;
  margin: 14px 0 0;
  font-weight: 800;
}
.lf-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}
.lf-card {
  /* Each card sets its own; these are what a card that forgets gets. */
  --tone: var(--accent);
  --tone-bg: var(--accent-bg);
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 22px;
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
/* A wash of the card's own colour in one corner: enough to tell them apart
   at a glance, never under the text. */
.lf-card::before {
  content: '';
  position: absolute;
  inset: -40% -40% auto auto;
  width: 70%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--tone-bg), transparent);
  pointer-events: none;
}
.lf-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
  border-color: var(--tone);
}
.lf-card > * { position: relative; }
.lf-wide { grid-column: span 2; }
.lf-ico {
  width: 42px;
  height: 42px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  color: var(--tone);
  background: var(--tone-bg);
  margin-bottom: 14px;
}
.lf-ico svg { width: 22px; height: 22px; }
.lf-card h3 { margin: 0 0 6px; font-size: 16px; letter-spacing: -0.01em; }
.lf-card p { margin: 0; color: var(--fg-2); font-size: 14px; line-height: 1.45; }

/* The trace illustration: a waterfall in four rows. */
.lf-wf { margin-top: 18px; display: grid; gap: 7px; }
.lf-wf div {
  display: grid;
  grid-template-columns: 136px 1fr;
  gap: 10px;
  align-items: center;
  font-size: 12px;
  color: var(--fg-3);
  font-family: var(--mono, ui-monospace, monospace);
}
.lf-wf span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lf-wf i {
  --l: 0%;
  --w: 100%;
  --c: var(--accent);
  display: block;
  height: 10px;
  border-radius: 5px;
  background: var(--panel-2);
  position: relative;
}
.lf-wf i::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--l);
  width: var(--w);
  border-radius: 5px;
  background: var(--c);
  transform-origin: left;
  animation: lf-grow 1.4s cubic-bezier(.2, .8, .2, 1) both;
}
.lf-wf div:nth-child(2) i::after { animation-delay: .15s; }
.lf-wf div:nth-child(3) i::after { animation-delay: .3s; }
.lf-wf div:nth-child(4) i::after { animation-delay: .45s; }
@keyframes lf-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* The cause chain: three steps, the last one is the answer. */
.lf-chain { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 8px; }
.lf-chain li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 12px;
  border-radius: 10px;
  background: var(--panel-2);
  font-size: 13px;
  border-left: 3px solid var(--line-2);
}
.lf-chain li span { color: var(--fg-3); text-align: right; }
.lf-chain li:last-child { border-left-color: var(--ai); background: var(--ai-bg); }
.lf-chain li:last-child span { color: var(--fg-2); }

.lf-strip {
  margin-top: 28px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 28px;
  color: var(--fg-2);
  font-size: 14px;
}
.lf-strip b { color: var(--fg); }

/*
 * The offer (landing.js offerTile, public install only): the whole row, the
 * trial on the left, what the monthly fee is made of on the right. The whole
 * tile is its link, as the feature tiles are.
 */
.lf-grid .lf-offer { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 26px 52px; align-items: center;
  padding: 32px 34px; cursor: pointer; border-color: color-mix(in srgb, var(--good) 45%, var(--line));
  background: linear-gradient(115deg, color-mix(in srgb, var(--good) 11%, var(--panel)), var(--panel) 52%, color-mix(in srgb, var(--accent) 9%, var(--panel))); }
.lf-offer .lf-offer-main { position: static; }
.lf-offer h3 { font-size: clamp(22px, 2.3vw, 30px); line-height: 1.15; letter-spacing: -.02em; margin: 0 0 10px; }
.lf-offer .lf-offer-main p { font-size: 16px; line-height: 1.5; text-wrap: pretty; }
.lf-offer .lf-offer-main .lf-more { font-size: 15px; margin-top: 10px; }
.lf-offer-go { display: inline-flex; gap: 6px; margin-top: 18px; font-size: 14px; font-weight: 600; color: var(--tone); }
.lf-offer-go::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.lf-offer-go:hover { text-decoration: none; }
.lf-offer-go:focus-visible { outline: none; }
.lf-offer:has(.lf-offer-go:focus-visible) { outline: 2px solid var(--tone); outline-offset: 2px; }
.lf-offer-meter { padding: 20px 22px; border-radius: 14px; border: 1px solid var(--line); background: color-mix(in srgb, var(--panel-2) 45%, var(--panel)); }
.lf-offer-cap { display: block; margin-bottom: 12px; font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-2); }
.lf-offer-bar { display: flex; gap: 3px; height: 12px; border-radius: 6px; overflow: hidden; }
.lf-offer-bar i { flex: 1 1 var(--w); background: var(--c); transform-origin: left; animation: lf-grow 1.4s cubic-bezier(.2, .8, .2, 1) both; }
.lf-offer-bar i:nth-child(2) { animation-delay: .12s; }
.lf-offer-bar i:nth-child(3) { animation-delay: .24s; }
.lf-offer-bar i:nth-child(4) { animation-delay: .36s; }
.lf-offer-meter ul { list-style: none; margin: 16px 0 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; }
.lf-offer-meter li { position: relative; display: grid; align-content: start; padding-left: 16px; font-size: 13px; line-height: 1.4; color: var(--fg-2); }
.lf-offer-meter li::before { content: ""; position: absolute; left: 0; top: 5px; width: 8px; height: 8px; border-radius: 2px; background: var(--c); }
.lf-offer-meter li b { color: var(--fg); font-size: 14px; }
.lf-offer .lf-offer-foot { margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; color: var(--fg-2); }
@media (max-width: 860px) {
  .lf-grid .lf-offer { grid-template-columns: minmax(0, 1fr); padding: 24px 20px; }
}
@media (prefers-reduced-motion: reduce) {
  .lf-offer-bar i { animation: none; }
}

/*
 * "How it works": the front page in sentences, under the tiles (public install
 * only, landing.js landingAbout). Text straight on the veil, as .lf-strip is:
 * --fg-2 for the words, --fg for the headings — pairings test:contrast measures.
 * Links in running text are underlined, so colour is not the only sign.
 */
.la {
  position: relative;
  z-index: 1;
  max-width: 1180px;
  margin: 0 auto;
  padding: 8px 0 56px;
}
.la-lead {
  max-width: 68ch;
  margin: 0 auto 40px;
  text-align: center;
  font-size: clamp(16px, 1.5vw, 19px);
  line-height: 1.6;
  color: var(--fg-2);
}
.la-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 32px 56px;
}
.la-block {
  border-top: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  padding-top: 18px;
}
.la-block h3 { margin: 0 0 10px; font-size: 18px; letter-spacing: -0.01em; color: var(--fg); }
.la-block p { margin: 0 0 10px; color: var(--fg-2); font-size: 15px; line-height: 1.6; }
.la-block p:last-child { margin-bottom: 0; }
.la-block a { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }

/* Share links: plain addresses of the platforms' share pages, no script (landing.js shareLinks). */
.share {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 8px;
  margin-top: 36px;
  font-size: 13px;
}
.share > span { color: var(--fg-2); margin-right: 4px; }
.share a {
  color: var(--fg-2);
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
  border-radius: 999px;
  padding: 5px 12px;
}
.share a:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; }
.post .share { justify-content: flex-start; margin: 32px 0 0; }

/* Words that are read aloud and indexed but not drawn (the rest of a "Read more"). */
.vh {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 1020px) {
  .lf-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 860px) {
  .landing-hero { grid-template-columns: minmax(0, 1fr); gap: 28px; min-height: 0; padding: 40px 0 24px; }
  .landing-pitch p { max-width: none; }
  .la-grid { grid-template-columns: minmax(0, 1fr); gap: 26px; }
  .la-lead { text-align: left; margin-bottom: 30px; }
}
@media (max-width: 560px) {
  .lf-grid { grid-template-columns: minmax(0, 1fr); }
  .lf-wide { grid-column: auto; }
  .lf-grid { gap: 10px; }
  /* On a phone the small cards are rows: icon beside the words, half the height. */
  .lf-card:not(.lf-wide) {
    display: grid;
    grid-template-columns: 38px 1fr;
    column-gap: 14px;
    padding: 14px 16px;
    align-items: start;
  }
  .lf-card:not(.lf-wide) .lf-ico { grid-row: span 2; width: 38px; height: 38px; margin: 0; }
  .lf-card:not(.lf-wide) h3 { margin: 1px 0 3px; font-size: 15px; }
  .lf-card:not(.lf-wide) p { font-size: 13px; }
  .lf-wf div { grid-template-columns: 104px 1fr; }
  .lf-chain li { flex-direction: column; gap: 2px; }
  .lf-chain li span { text-align: left; }
}
@media (prefers-reduced-motion: reduce) {
  .lf-wf i::after { animation: none; }
  .lf-card, .landing-more span { transition: none; }
}

.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) 76%, 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; }
}

/*
 * "Stay in touch" and the privacy notice, under the features (obscenter.site
 * only). The form sits on an opaque card for the same reason the sign-in form
 * does: its contrast is then the card's, which test:contrast already measures.
 * The small print is --fg-2 on the veil, the same pairing as .lf-strip, not
 * --fg-3: small text is where contrast runs out first.
 */
.lsub {
  position: relative;
  z-index: 1;
  max-width: 1180px;
  margin: 0 auto;
  padding: 8px 0 40px;
}
.lsub-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 460px);
  gap: 24px 48px;
  align-items: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 32px 36px;
  box-shadow: var(--shadow-lg);
}
.lsub-copy h2 {
  font-size: clamp(22px, 2.6vw, 30px);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 12px 0 8px;
  font-weight: 800;
}
.lsub-copy p { margin: 0; color: var(--fg-2); font-size: 15px; line-height: 1.5; max-width: 46ch; }
.lsub-form label { margin-top: 0; }
.lsub-row { display: flex; gap: 8px; }
.lsub-row input { flex: 1 1 auto; min-width: 0; padding: 10px 12px; }
.lsub-row .btn { flex: 0 0 auto; padding: 10px 16px; white-space: nowrap; }
.lsub-row .btn:disabled { opacity: .7; cursor: progress; }
.lsub-form input[aria-invalid='true'] { border-color: var(--bad); }
.lsub-msg { min-height: 18px; margin: 8px 0 0; color: var(--fg-2); }
.lsub-msg[data-tone='good'] { color: var(--good); font-weight: 600; }
.lsub-msg[data-tone='bad'] { color: var(--bad); }
.lsub-fine { margin: 4px 0 0; color: var(--fg-2); }
.lsub-fine .linkbtn { font-size: inherit; }
/* The field only a robot fills: out of sight and out of the tab order. */
.lsub-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.lsub-contact { text-align: center; margin: 22px 0 0; color: var(--fg-2); font-size: 15px; }
.lsub-contact a { font-weight: 600; }

.lfoot {
  position: relative;
  z-index: 1;
  max-width: 760px;
  margin: 0 auto;
  padding: 20px 0 40px;
  border-top: 1px solid var(--line);
  color: var(--fg-2);
  font-size: 12px;
  line-height: 1.55;
}
.lfoot h2 { font-size: 12px; font-weight: 700; margin: 0 0 6px; color: var(--fg); letter-spacing: 0; }
.lfoot p { margin: 0 0 6px; }
.lfoot b { color: var(--fg); font-weight: 600; }

@media (max-width: 860px) {
  .lsub-card { grid-template-columns: minmax(0, 1fr); padding: 24px 22px; }
}
@media (max-width: 420px) {
  .lsub-row { flex-direction: column; }
  .lsub-row .btn { width: 100%; }
}

/* Sign in with a phone: the QR half of the login card, and the phone's page. */
.qrsign-or {
  display: flex; align-items: center; gap: 10px;
  margin: 6px 0 14px; color: var(--fg-3); font-size: 12px;
}
.qrsign-or::before, .qrsign-or::after { content: ''; flex: 1; border-top: 1px solid var(--line); }
.qrsign-panel { text-align: center; }
.qrsign-code {
  width: min(240px, 100%); aspect-ratio: 1; margin: 4px auto 12px;
  border-radius: 8px; overflow: hidden; background: var(--qr-paper);
  transition: opacity .2s;
}
.qrsign-code svg { display: block; width: 100%; height: 100%; }
.qrsign-code.stale { opacity: .25; }
.qrsign-msg { min-height: 18px; margin: 0 0 6px; }

.qrapprove {
  min-height: 100vh; display: grid; place-items: center;
  padding: 16px; background: var(--bg);
}
.qrapprove-card { width: 100%; max-width: 420px; padding: 28px 24px; box-shadow: var(--shadow-lg); }
.qrapprove-who {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 6px 14px;
  margin: 0 0 16px;
}
.qrapprove-who dt { color: var(--fg-3); }
.qrapprove-who dd { margin: 0; overflow-wrap: anywhere; }
.qrapprove-warn { padding: 10px 12px; border-radius: 8px; background: var(--warn-bg); color: var(--fg); }
.qrapprove-actions { display: flex; gap: 10px; margin-top: 16px; }
.qrapprove-actions .btn { flex: 1; }

/*
 * 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; }
/*
 * min-width keeps the diagram readable on a phone, where it does scroll;
 * max-width stops it from ever being a pixel wider than its container on a
 * screen where it fits, which is where the scrollbar was showing up with
 * nothing to scroll to.
 */
.topology svg { display: block; min-width: 720px; max-width: 100%; }

/* 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 --- */

.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.
 */
/* An application tile's heading: the name gives way, the badges never do.
   A long domain used to push "2 live" onto two lines and the settings button
   out of the card. The whole name is in the tooltip. */
/*
 * The application tile. The name owns the first line (wrapping to two, since a
 * domain has no spaces to break at); kind, status and source sit under it.
 */
.apptile {
  cursor: pointer;
  display: flex; flex-direction: column; gap: 14px;
  transition: border-color .15s, box-shadow .15s;
}
.apptile:hover, .apptile:focus-visible { border-color: var(--accent); box-shadow: var(--shadow-lg); outline: none; }
.apptile-head { display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
.apptile-kind {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-bg); color: var(--accent);
}
.apptile-kind svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.apptile-title { flex: 1 1 auto; min-width: 0; }
.apptile-name {
  font-weight: 650; font-size: 16px; line-height: 1.25; color: var(--fg);
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.apptile-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
.apptile-head > .icon-btn { flex: 0 0 auto; }
.apptile-status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--fg-2); margin-right: 2px; }
.apptile-status::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--fg-3); }
.apptile-status.live { color: var(--good); }
.apptile-status.live::before { background: var(--good); box-shadow: 0 0 0 3px var(--good-bg); }
.apptile-status.active::before { background: var(--good); }
.apptile-status.off { color: var(--fg-3); }
.apptile-status.off::before { background: transparent; border: 1.5px solid var(--fg-3); }
.apptile-foot {
  margin-top: auto; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--fg-3);
}
.appstats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.appstat { background: var(--panel-2); border-radius: 9px; padding: 9px 11px; min-width: 0; }
.appstat.wide { grid-column: 1 / -1; }
.appstat .appspark { margin-top: 8px; }
.appstat .k { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); }
.appstat .k a { color: inherit; text-decoration: underline dotted; text-underline-offset: 3px; }
.appstat .k a:hover { color: var(--accent); }
.appstat .v { font-size: 20px; font-weight: 650; margin-top: 3px; font-variant-numeric: tabular-nums; }
.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); }
/* Settings: the sections down the left, the open one opening beside it.
   Nine chips in a row wrap onto two lines on a laptop, and a wrapped strip
   has no order left — the eye cannot tell a second row from a continuation. */
.setwrap { display: flex; gap: 18px; align-items: flex-start; }
.setnav {
  flex: 0 0 208px; display: flex; flex-direction: column; gap: 2px;
  position: sticky; top: 12px;
}
.setnav-item {
  display: block; padding: 8px 12px; border-radius: 8px;
  color: var(--fg-2); text-decoration: none; font-size: 14px;
  border-left: 3px solid transparent; transition: background .12s, color .12s;
}
.setnav-item:hover { background: var(--panel-2); color: var(--fg); }
.setnav-item.on {
  background: var(--panel-2); color: var(--fg); font-weight: 600;
  border-left-color: var(--accent);
}
.setpane { flex: 1 1 auto; min-width: 0; }
@media (max-width: 760px) {
  /* Below this a column costs half the screen before any content does. */
  .setwrap { display: block; }
  .setnav { flex-direction: row; flex-wrap: wrap; gap: 6px; position: static; margin-bottom: 14px; }
  .setnav-item { border-left: none; border: 1px solid var(--line); padding: 6px 10px; }
  .setnav-item.on { border-color: var(--accent); }
}

/* Agent health: one block per machine, one row per mechanism. Three states,
   because "nobody asked for this" and "asked for and not arriving" are the two
   that must not look alike. */
.ahost { border-top: 1px solid var(--line); padding: 12px 0; }
.ahost:first-child { border-top: none; padding-top: 0; }
.ahrows { margin-top: 8px; display: flex; flex-direction: column; gap: 1px; }
.ahrow {
  display: grid; grid-template-columns: 20px 1fr auto; gap: 10px;
  align-items: baseline; padding: 5px 8px; border-radius: 6px; font-size: 13.5px;
}
.ahrow:hover { background: var(--panel-2); }
.ahmark { font-weight: 700; text-align: center; }
.ahdetail { color: var(--fg-3); font-size: 12.5px; text-align: right; }
.ah-ok   .ahmark { color: var(--good); }
.ah-none .ahmark { color: var(--bad); }
.ah-off  .ahmark { color: var(--fg-3); }
.ah-none .ahlabel { color: var(--fg); }
.ah-off  .ahlabel { color: var(--fg-3); }
@media (max-width: 560px) {
  .ahrow { grid-template-columns: 20px 1fr; }
  .ahdetail { grid-column: 2; text-align: left; }
}

.hostcard h3 { align-items: center; }
/* Whether the agent is reporting, at the top of the tile. The dot is the
   signal; the name repeats it in colour only when the answer is bad, because
   a wall of green names says nothing and a red one has to be seen. */
.hostcard h3 .hdot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; margin-right: 2px;
}
.hostcard h3.hon .hdot { background: var(--good); box-shadow: 0 0 0 3px var(--good-bg); }
.hostcard h3.hoff .hdot { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-bg); }
.hostcard h3.hoff, .hostcard h3.hoff .hname { color: var(--bad); }
/* Stopped or removed from the dashboard: quiet on purpose, so neither green nor red. */
.hostcard h3.hstop .hdot { background: var(--fg-3); box-shadow: 0 0 0 3px var(--line); }
/* Health & overhead: the agent's cost on the machine, one line under its name. */
.ahcost { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 4px 0 6px; color: var(--fg-2); }
.agentctl { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/*
 * 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.
 */
/* Always there, faint (a touch screen has no hover: it was an invisible 30px
   that pushed the badge 49px from the card's edge); full on hover and focus. */
.hostcard .gear { opacity: .45; padding: 0 6px; line-height: 1.6; border-color: transparent; background: transparent; }
.hostcard:hover .gear, .hostcard .gear:focus-visible { opacity: 1; border-color: var(--line-2); }

/* The heat strip: a page from top to bottom, and its sections beside it. */
.heat-wrap { overflow-x: auto; }
.heat { display: flex; gap: 10px; align-items: flex-start; padding: 6px 0; min-width: min-content; }
.heat-axis { position: relative; width: 46px; flex: none; font-size: 11px; color: var(--fg-3); }
.heat-axis span { position: absolute; right: 0; transform: translateY(-50%); white-space: nowrap; }
.heat-strip { position: relative; width: 110px; flex: none; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; box-sizing: content-box; }
.heat-band { width: 100%; }
.heat-fold { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--fg-2); pointer-events: none; }
.heat-fold span { position: absolute; right: 4px; top: 1px; font-size: 10px; color: var(--fg); background: var(--panel); padding: 0 4px; border-radius: 3px; white-space: nowrap; }
.heat-marks { position: relative; flex: none; }
.heat-mark { position: absolute; box-sizing: border-box; border-left: 2px solid var(--fg-3); padding-left: 8px; font-size: 12px; line-height: 15px; overflow: visible; white-space: nowrap; }
.heat-mark.video { border-left-color: var(--s2); }
.heat-mark b { display: block; overflow: hidden; text-overflow: ellipsis; }
.heat-mark span { display: block; color: var(--fg-2); font-size: 11px; }
.heat-key { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.heat-key i { display: inline-block; width: 18px; height: 10px; border-radius: 2px; }
.heat-key i.none { background: var(--panel-2); border: 1px solid var(--line); margin-left: 10px; }

button.chip { background: none; font-family: inherit; cursor: pointer; }

/* The page itself with the heat over it: one pane per device class. */
/*
 * The Pages list. One card, sized by its own width and not the window's: the
 * same card is two thousand pixels on a desk and three hundred on a phone,
 * and beside an open side panel it is narrow in a wide window.
 */
.pgl { container: pgl / inline-size; }
.pgtop { display: flex; gap: 22px; align-items: stretch; margin-bottom: 14px; }
.pgstats { display: flex; gap: 24px; flex: none; align-items: center; }
.pgstat { display: flex; flex-direction: column; color: inherit; text-decoration: none; min-width: 0; }
a.pgstat:hover .v { text-decoration: underline; }
.pgstat .k { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); white-space: nowrap; }
.pgstat .v { font-size: 22px; font-weight: 700; line-height: 28px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.pgstat .v.good, .pgv.good { color: var(--good); }
.pgstat .v.warn, .pgv.warn { color: var(--warn); }
.pgstat .v.bad, .pgv.bad { color: var(--bad); }
.pgsince { align-self: center; white-space: nowrap; flex: none; cursor: help; }
.pgribbon { flex: 1 1 0; min-width: 0; display: flex; gap: 2px; height: 50px; }
.pgseg { flex: 1 1 0; min-width: 5px; container-type: inline-size; display: flex; flex-direction: column; justify-content: center; padding: 0 10px;
  background: var(--panel-2); border-bottom: 3px solid var(--line-2); color: var(--fg); text-decoration: none; }
.pgseg:first-child { border-radius: 8px 0 0 8px; }
.pgseg:last-child { border-radius: 0 8px 8px 0; }
.pgseg:only-child { border-radius: 8px; }
.pgseg:hover { filter: brightness(1.12); }
.pgseg.good { background: var(--good-bg); border-bottom-color: var(--good); }
.pgseg.warn { background: var(--warn-bg); border-bottom-color: var(--warn); }
.pgseg.bad { background: var(--bad-bg); border-bottom-color: var(--bad); }
.pgseg b, .pgseg i { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.pgseg b { font-size: 13px; font-weight: 650; }
.pgseg i { font-style: normal; font-size: 11px; color: var(--fg-2); font-variant-numeric: tabular-nums; }
/* Too thin to carry a name: the colour and the width are all it says. */
@container (max-width: 58px) { .pgseg b, .pgseg i { display: none; } }

.pgrows table { width: 100%; min-width: 0; table-layout: fixed; }
.pgrows th, .pgrows td { overflow: hidden; }
.pgrows tbody td { height: 48px; vertical-align: middle; }
.pgrows tbody tr { box-shadow: inset 3px 0 0 transparent; }
.pgrows tbody tr.pgt-warn { box-shadow: inset 3px 0 0 var(--warn); }
.pgrows tbody tr.pgt-bad { box-shadow: inset 3px 0 0 var(--bad); }
.pgrows th:nth-child(1) { width: 25%; }
.pgrows th:nth-child(2) { width: 5%; }
.pgrows th:nth-child(3) { width: 14%; }
.pgrows th:nth-child(4) { width: 13%; }
.pgrows th:nth-child(5), .pgrows th:nth-child(6), .pgrows th:nth-child(7) { width: 9%; }
.pgrows th:nth-child(8), .pgrows th:nth-child(9) { width: 5%; }
.pgrows th:nth-child(10) { width: 3%; }
.pgrows th:nth-child(3) { text-align: left; }

/* The path, over a fill as long as its share of the busiest page's views. */
.pgpath { position: relative; height: 30px; display: flex; align-items: center; min-width: 0; }
.pgpath i { position: absolute; left: 0; top: 2px; bottom: 2px; background: var(--s1); opacity: .22; border-radius: 4px; }
.pgpath span { position: relative; padding: 0 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
/* Views over time: a count, so bars from zero. A bucket with errors in it is red. */
.pgbars { display: flex; align-items: flex-end; gap: 1px; height: 32px; border-bottom: 1px solid var(--line); }
.pgbars a { flex: 1 1 0; min-width: 0; height: 100%; display: flex; align-items: flex-end; }
.pgbars a:hover { background: var(--panel-2); }
.pgbars b { display: block; width: 100%; background: var(--s1); border-radius: 2px 2px 0 0; }
.pgbars a.e b { background: var(--bad); }
/* A picture and the one number it summarises. */
.pgm { display: flex; align-items: center; gap: 10px; min-width: 0; }
.pgm.few .pgsplit { opacity: .45; }
.pgv { flex: none; min-width: 52px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
/* Load time: the band from the median to the slow tail, the dot at p75, a tick at each threshold. */
.pgrange { position: relative; flex: 1 1 0; min-width: 0; height: 14px; background: var(--panel-2); border-radius: 7px; }
.pgrange u { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line-2); }
.pgrange i { position: absolute; top: 4px; height: 6px; border-radius: 3px; background: var(--fg-3); opacity: .55; }
.pgrange b { position: absolute; top: 2px; width: 10px; height: 10px; margin-left: -5px; border-radius: 50%; background: var(--fg-2); box-shadow: 0 0 0 2px var(--panel); }
.pgrange.good i, .pgrange.good b { background: var(--good); }
.pgrange.warn i, .pgrange.warn b { background: var(--warn); }
.pgrange.bad i, .pgrange.bad b { background: var(--bad); }
/* A vital, as its three shares. */
.pgsplit { flex: 1 1 0; min-width: 0; display: flex; gap: 2px; height: 8px; }
.pgsplit > * { flex: 1 1 0; min-width: 3px; border-radius: 2px; display: block; }
.pgsplit .good { background: var(--good); }
.pgsplit .warn { background: var(--warn); }
.pgsplit .bad { background: var(--bad); }
.pgsplit a:hover { outline: 2px solid var(--fg); outline-offset: 1px; }
.pgmeter span { display: block; }
.pgmeter i { display: block; height: 3px; margin-top: 3px; background: var(--panel-2); border-radius: 2px; }
.pgmeter b { display: block; height: 100%; margin-left: auto; background: var(--s1); border-radius: 2px; opacity: .7; }

/* Not wide enough for ten columns: time on page and weight are on the page's own view. */
@container pgl (max-width: 1499px) {
  .pgrows th:nth-child(8), .pgrows td:nth-child(8), .pgrows th:nth-child(9), .pgrows td:nth-child(9) { display: none; }
  .pgrows th:nth-child(1) { width: 25%; }
  .pgrows th:nth-child(2) { width: 6%; }
  .pgrows th:nth-child(3) { width: 15%; }
  .pgrows th:nth-child(4) { width: 14%; }
  .pgrows th:nth-child(5), .pgrows th:nth-child(6), .pgrows th:nth-child(7) { width: 10%; }
  .pgrows th:nth-child(10) { width: 7%; }
}
/* Not wide enough for columns at all: each page is a block, the header its sort chips. */
@container pgl (max-width: 999px) {
  .pgtop { flex-direction: column; gap: 12px; }
  .pgstats { flex-wrap: wrap; gap: 10px 22px; }
  .pgribbon { flex: none; height: 44px; }
  /* 18 segments at 20px of padding each were 425px on a 390px phone: the ribbon
     shrinks its padding so every segment fits (labels hide below 58px anyway). */
  .pgseg { padding: 0 4px; }
  .pgrows table, .pgrows thead, .pgrows tbody { display: block; }
  .pgrows thead tr { display: flex; flex-wrap: wrap; gap: 2px 14px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
  .pgrows th { width: auto !important; padding: 4px 0; border: 0; text-align: left; }
  .pgrows th:not(.sortable) { display: none; }
  .pgrows tbody tr { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px 16px; padding: 12px 0 12px 8px; border-bottom: 1px solid var(--line); }
  .pgrows tbody td { display: block; height: auto; padding: 0; border: 0; text-align: left; }
  .pgrows tbody td:nth-child(1) { grid-row: 1; grid-column: 1 / span 2; }
  .pgrows tbody td:nth-child(2) { grid-row: 1; grid-column: 3; text-align: right; align-self: center; font-weight: 700; }
  .pgrows tbody td:nth-child(10) { grid-row: 1; grid-column: 4; text-align: right; align-self: center; }
  .pgrows tbody td:nth-child(3) { grid-row: 2; grid-column: 1 / -1; }
  .pgrows tbody td:nth-child(n+4):nth-child(-n+7) { grid-row: 3; }
  .pgrows tbody td:nth-child(n+4):nth-child(-n+7)::before { content: attr(data-label); display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); margin-bottom: 3px; }
  .pgrows tbody td.empty { grid-column: 1 / -1; }
}
@container pgl (max-width: 599px) {
  /* Two by two. Written with the same weight as the rule above it, or that one wins. */
  .pgrows tbody td:nth-child(n+6):nth-child(-n+7) { grid-row: 4; }
  .pgrows tbody td:nth-child(n+4):nth-child(-n+7) { grid-column: span 2; }
}

.hp-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start; }
/* In one row the two frames share the whole width of the card. */
.hp-row.two { flex-wrap: nowrap; }
.hp-row.two .hp-pane { flex: 1 1 auto; }
.hp-pane { min-width: 0; max-width: 100%; }
/* A pane is as wide as its picture. The line above it never sets that width:
   a sentence under the phone's pane once made it wider than the desktop's
   picture, and the two stacked on a screen that fits both. */
.hp-head { width: 0; min-width: 100%; margin-bottom: 6px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hp-head b { font-size: 13px; margin-right: 6px; }
.hp-head [title] { cursor: help; }
.hp-head a[title] { cursor: pointer; }
.hp-scroll { max-height: min(60vh, 400px); overflow: auto; scrollbar-width: thin; scrollbar-gutter: stable; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); padding: 8px; }
.hp { display: flex; align-items: flex-start; gap: 6px; min-width: min-content; }
.hp-time { position: relative; width: 8px; flex: none; border-radius: 3px; background: var(--panel); overflow: hidden; }
.hp-band { position: absolute; left: 0; right: 0; }
.hp-shot { position: relative; flex: none; overflow: hidden; border-radius: 4px; background: var(--panel); box-shadow: 0 0 0 1px var(--line); }
.hp-shot img { position: absolute; left: 0; display: block; object-fit: fill; }
.hp-veil { position: absolute; left: 0; right: 0; background: var(--panel); pointer-events: none; }
.hp-over { position: absolute; left: 0; right: 0; }
.hp-line, .hp-fold { position: absolute; left: 0; right: 0; height: 0; pointer-events: none; }
.hp-line { border-top: 1px dashed var(--fg); }
.hp-fold { border-top: 1px solid var(--s2); }
.hp-line span, .hp-fold span { position: absolute; right: 4px; top: -7px; font-size: 9px; line-height: 14px; padding: 0 5px; border-radius: 7px; white-space: nowrap; background: var(--fg); color: var(--panel); font-weight: 600; }
.hp-fold span { right: auto; left: 4px; background: var(--s2); color: var(--ink-invert); }
.hp-hi { display: none; position: absolute; left: 0; right: 0; box-shadow: inset 0 0 0 2px var(--s2); background: color-mix(in srgb, var(--s2) 14%, transparent); pointer-events: none; }
.hp-marks { position: relative; flex: none; }
.hp-bracket { position: absolute; width: 5px; border: 2px solid var(--line-2); border-right: 0; border-radius: 3px 0 0 3px; box-sizing: border-box; }
.hp-bracket.video { border-color: var(--s2); }
.hp-track { position: absolute; pointer-events: none; }
.hp-label { position: sticky; pointer-events: auto; font-size: 11px; line-height: 14px; padding: 1px 4px; border-radius: 4px; cursor: default; }
.hp-label.on { background: var(--panel); box-shadow: 0 0 0 1px var(--line); }
.hp-label b { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hp-label b i { font-style: normal; font-weight: 400; font-size: 10px; color: var(--s2); border: 1px solid var(--s2); border-radius: 6px; padding: 0 4px; margin-left: 4px; }
.hp-label span { display: block; color: var(--fg-2); font-size: 10px; white-space: nowrap; }
.hp-label em { font-style: normal; font-weight: 600; color: var(--fg); }

.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; }

/*
 * The cause a model read, and what it rests on.
 *
 * The citations are not decoration: they are the reason the paragraph is
 * allowed on the page. A cause with nothing under it never renders as prose —
 * it renders as "not enough evidence" — so the chips below the sentence are
 * part of the claim rather than a footnote to it.
 */
/*
 * Its own name, learned the hard way.
 *
 * This card was called .pcause, which was already the class of ONE ROW in the
 * detector's own list of what it blamed — a 132px label column and a text
 * column. The card inherited that grid: its heading went into the label
 * column and its sentence into the other, which is what "the page has fallen
 * apart" looked like. The border it also inherited landed on every row of
 * that older list.
 */
.paicause { border-left: 3px solid var(--ai); }
.pcites { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
.pcites .k, .pnext .k, .pnear .k {
  font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3);
  margin-right: 2px;
}
/*
 * One line, whatever the width.
 *
 * A model writes a citation as a sentence with the number in it, and a chip
 * that wraps a sentence is a grey blob: three of them turned the cause card
 * into something tall enough for the page's column layout to tear in half.
 * The whole text is on the chip's title.
 */
.pcite {
  font-family: var(--mono); font-size: 11.5px; padding: 2px 8px; border-radius: 999px;
  background: var(--panel-2); color: var(--fg-2); border: 1px solid var(--line);
  max-width: min(38ch, 100%); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pnext { margin-top: 12px; }
.pnext ol { margin: 6px 0 0; padding-left: 20px; font-size: 12.5px; color: var(--fg-2); }
.pnext li { margin: 3px 0; }
.pnear { margin-top: 10px; }
.pnear ul { margin: 6px 0 0; padding-left: 18px; font-size: 12.5px; color: var(--fg-2); }
.pnear li { margin: 4px 0; }

/*
 * The AI mark.
 *
 * Two letters, its own colour, and nothing to press. It sits wherever a
 * sentence on a page was written by a model rather than measured by this
 * product, and it is the SAME mark in all of those places — a reader should
 * learn it once. The model's name lives in the tooltip: it matters when
 * somebody is comparing models and is noise in the middle of a sentence about
 * a fault.
 */
.aitag {
  display: inline-flex; align-items: center; vertical-align: middle;
  padding: 1px 6px; border-radius: 5px;
  background: var(--ai-bg); color: var(--ai);
  border: 1px solid color-mix(in srgb, var(--ai) 28%, transparent);
  font-size: 9.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  line-height: 1.5; cursor: default; user-select: none; white-space: nowrap;
}
.aitag + .drill, .drill .aitag { margin-right: 4px; }

/* ----------------------------------------------------------- assistant ----- */

/*
 * The assistant: a mark in the corner, and a sheet above it.
 *
 * Fixed rather than in the page, because every view here is redrawn from
 * scratch and a conversation that disappeared when the range picker moved is
 * a conversation nobody starts. The panel is a column: a head that says whose
 * page you are asking about, the conversation, and one input. Nothing else —
 * a chat window collects furniture faster than any other kind of UI, and none
 * of it is what somebody came to do.
 */
.assistantbtn {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  display: grid; place-items: center;
  width: 52px; height: 52px; border-radius: 999px; border: 0;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--accent) 88%, white 12%), var(--accent));
  color: var(--accent-ink); font-size: 20px; line-height: 1; cursor: pointer;
  box-shadow: 0 10px 26px color-mix(in srgb, var(--accent) 45%, transparent);
  transition: transform .14s ease, box-shadow .14s ease;
}
.assistantbtn:hover { transform: translateY(-1px); box-shadow: 0 14px 30px color-mix(in srgb, var(--accent) 55%, transparent); }
.assistantbtn:active { transform: translateY(0); }

/* Its [hidden] is the global rule at the top ("hidden means hidden"). */

.assistant {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  display: flex; flex-direction: column;
  width: min(400px, calc(100vw - 40px));
  /*
   * dvh, not vh.
   *
   * On a phone 100vh is the height the page WOULD have with the browser's own
   * bars hidden — the large viewport — so a panel sized from it is taller than
   * what anybody can actually see while the address bar is showing, and the
   * only way to see all of it is to zoom out. That is what somebody hit. dvh
   * is the height there is right now, and it changes as the bars come and go.
   * The vh line stays first as the answer for anything that has not heard of
   * dvh: too tall is better than not there.
   */
  height: min(620px, calc(100vh - 60px));
  height: min(620px, calc(100dvh - 60px));
  background: var(--panel); border: 1px solid var(--line); border-radius: 18px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .38); overflow: hidden;
}

/* ---- the head: a mark, who you are asking about, and the way out -------- */
.cphead {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 12px 12px 14px; border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--panel-2), transparent);
}
.cpmark {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: 8px;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent); font-size: 13px;
}
.cpwho { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.cpwho b { font-size: 13px; }
/* The page you are on, because "what is wrong here" needs a here. */
.cppage {
  font-size: 11px; color: var(--fg-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/*
 * Hidden while the panel is up, because the panel carries the same mark in its
 * header and the corner is the last space left when a keyboard is open.
 */

/*
 * Closing, in the header, in the colour people already read as "this removes
 * something". The corner mark opens it; this is the only thing in the panel
 * that takes it away, so it says so rather than repeating the same glyph in
 * two meanings.
 */
.cpclose {
  margin-left: auto; flex: none;
  display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
  border: 0; border-radius: 999px;
  background: color-mix(in srgb, var(--bad) 14%, transparent); color: var(--bad);
  font-size: 14px; line-height: 1; cursor: pointer;
}
.cpclose:hover { background: color-mix(in srgb, var(--bad) 26%, transparent); }

/* ---- the conversation --------------------------------------------------- */
.cplog {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-width: thin;
  padding: 14px; display: flex; flex-direction: column; gap: 10px;
}
.cprow { display: flex; }
.cprow.me { justify-content: flex-end; }
.cpbubble {
  max-width: 86%; padding: 9px 12px; border-radius: 14px;
  font-size: 12.5px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere;
  background: var(--panel-2); color: var(--fg-2);
  border: 1px solid var(--line);
}
.cprow.it .cpbubble { border-bottom-left-radius: 5px; }
.cprow.me .cpbubble {
  background: var(--accent); border-color: transparent; color: var(--accent-ink);
  border-bottom-right-radius: 5px;
}
/* What it looked at: a footnote, not a message. */
.cprow.note { justify-content: center; }
.cprow.note .cpbubble {
  background: none; border: 0; padding: 0; max-width: 100%;
  color: var(--fg-3); font-size: 11px; text-align: center;
}

/* Thinking is a state, and a state is shown rather than spelled. */
.cpdots { display: inline-flex; gap: 4px; padding: 2px 0; }
.cpdots i {
  width: 6px; height: 6px; border-radius: 999px; background: var(--fg-3);
  animation: cpblink 1.1s infinite ease-in-out;
}
.cpdots i:nth-child(2) { animation-delay: .15s; }
.cpdots i:nth-child(3) { animation-delay: .3s; }
@keyframes cpblink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .cpdots i { animation: none; opacity: .6; } }

/* ---- what it says before anybody has said anything ---------------------- */
.cpwelcome { display: flex; flex-direction: column; gap: 8px; }
.cpwelcome p { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--fg-2); }
.cpwelcome p.muted { color: var(--fg-3); }
.cpstarts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.cpstart {
  padding: 7px 11px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-2); color: var(--fg-2);
  font: inherit; font-size: 12px; text-align: left; cursor: pointer;
}
.cpstart:hover { border-color: var(--accent); color: var(--fg); }

/* The button under a proposal: the one place this thing changes anything. */
.cpdo { margin-top: 10px; }
.cpdo .btn { white-space: normal; text-align: left; line-height: 1.35; }

/* ---- the input: one pill, with the send inside it ----------------------- */
.cpask {
  display: flex; align-items: center; gap: 8px; margin: 0;
  padding: 10px 12px 12px; border-top: 1px solid var(--line);
}
.cpask input {
  flex: 1 1 auto; min-width: 0; margin: 0;
  padding: 10px 14px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--fg);
  font-size: 12.5px;
}
.cpask input:focus { border-color: var(--accent); outline: none; }
.cpsend {
  flex: none; width: 36px; height: 36px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 999px; background: var(--accent); color: var(--accent-ink);
  font-size: 15px; line-height: 1; cursor: pointer;
}
.cpsend:hover { filter: brightness(1.08); }

/*
 * On a phone it is a sheet: full width, off the bottom edge, and the button
 * tucks into the corner. A 400px panel floating in the middle of a 390px
 * screen is a desktop design somebody shrank.
 */
@media (max-width: 720px) {
  .assistant {
    /*
     * Anchored above the button and measured from what is on screen, with the
     * phone's own bottom inset (the home indicator) counted in: on a short
     * screen the old arithmetic put the top of the panel off the top of the
     * display and the input under the browser's toolbar.
     */
    right: 8px; left: 8px; width: auto; border-radius: 16px;
    /*
     * --cpvh and --cpgap are measured in JS from visualViewport; the dvh line
     * before them is what a browser without that gets, and the vh line before
     * THAT is for anything older still. Each one is a worse answer than the
     * one after it and none of them is "off the screen".
     */
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    bottom: calc(10px + env(safe-area-inset-bottom, 0px) + var(--cpgap, 0px));
    height: calc(100vh - 90px);
    height: calc(100dvh - 28px - env(safe-area-inset-bottom, 0px));
    height: calc(var(--cpvh, 100dvh) - 28px - env(safe-area-inset-bottom, 0px));
    max-height: 620px;
  }
  /*
   * 16px, and it is not a taste decision.
   *
   * Safari zooms the whole page in when a field with text smaller than 16px
   * takes focus — about 1.5×, which is exactly what somebody was looking at
   * when they said the panel opens bigger than the screen. It was not the
   * panel: the page under it had been magnified, and a fixed panel sized to
   * the viewport is then larger than what is left visible. The field is the
   * cause, so the field is where it is fixed.
   */
  .cpask input { font-size: 16px; padding: 11px 14px; }

  .assistantbtn {
    right: 14px; width: 48px; height: 48px;
    bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    bottom: calc(14px + env(safe-area-inset-bottom, 0px) + var(--cpgap, 0px));
  }
}

/* ---------------------------------------------------------------- ai ----- */

/*
 * One card per model, inside the models card. Nested cards are usually a smell;
 * here the outer card is the decision (which models exist, what may leave this
 * machine) and each inner one is a thing you switch on and give a job to.
 */
.aimodel { margin: 10px 0 0; padding: 12px; background: var(--panel-2); }
.aimodel h3 { margin-top: 0; }
.aidot {
  display: inline-block; width: 8px; height: 8px; margin-right: 7px; border-radius: 999px;
  background: var(--fg-3); flex: none;
}
.aidot.on { background: var(--good); }
.airoles { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 12px; }
.airole {
  display: inline-flex; align-items: center; gap: 6px; margin: 0;
  font-size: 12.5px; color: var(--fg-2); cursor: pointer;
}
.airole input[type=checkbox] { margin: 0; }

/* ---------------------------------------------------------- dashboards --- */

/*
 * The row of boards above the pins.
 *
 * Chips rather than a select: with three or four boards the whole set is worth
 * seeing at once — which one is shared, whose it is, how much is on it — and a
 * closed dropdown answers none of that until it is opened.
 */
.boardbar {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 14px; padding: 8px 10px;
}
.boardpicks { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 1 1 auto; }
.boardpicks .chip {
  display: inline-flex; align-items: center; gap: 6px;
  /* A finger, not a mouse pointer: the chips are how you change board. */
  min-height: 34px; padding: 5px 12px;
}
.boardpicks .bname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.boardpicks [data-newboard] { min-height: 0; }
.boardpicks .chip .n {
  font-variant-numeric: tabular-nums; font-size: 11px; opacity: .75;
}
.boardpicks .chip .tag {
  font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase;
  padding: 1px 5px; border-radius: 4px; background: var(--panel-2); color: var(--fg-3);
}
.boardpicks .chip.on .tag { background: color-mix(in srgb, var(--accent-ink) 22%, transparent); color: var(--accent-ink); }
.boardpicks .chip.on .muted { color: var(--accent-ink); opacity: .8; }
/*
 * The open board's own controls, inside its chip.
 *
 * The name is the rename field, the badge is the private/shared switch, and
 * the ✕ removes the board — three separate buttons on a second row before,
 * for a thing whose name was already on screen.
 */
.boardpicks .chip.own { padding-right: 4px; }
/* Big enough to hit with a thumb: these sit inside the chip, so they cost the
   row nothing — the chip is 34px tall either way. */
.boardpicks .chip .bname[data-rename] {
  display: inline-flex; align-items: center; min-height: 24px;
  cursor: text; border-radius: 4px; padding: 0 3px;
}
.boardpicks .chip.on .bname[data-rename]:hover,
.boardpicks .chip.on .bname[data-rename]:focus-visible {
  background: color-mix(in srgb, var(--accent-ink) 20%, transparent); outline: none;
}
.bnameedit {
  width: 9em; min-width: 0; margin: 0; padding: 1px 4px;
  border: 1px solid var(--accent-ink); border-radius: 5px;
  background: var(--panel); color: var(--fg);
  font: inherit; font-size: 12px;
}
/* The badge that is also a switch: it says the state, pressing it changes it. */
.boardpicks .chip .tag.tog {
  display: inline-flex; align-items: center; min-height: 24px; padding: 2px 7px;
  border: 0; cursor: pointer; font-family: inherit;
}
.boardpicks .chip.on .tag.tog:hover { filter: brightness(1.12); }
.boardpicks .chip .bx {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; margin: 0;
  border: 0; border-radius: 999px; background: none;
  color: inherit; opacity: .7; font-size: 13px; line-height: 1; cursor: pointer;
}
.boardpicks .chip .bx:hover {
  opacity: 1; background: color-mix(in srgb, var(--bad) 65%, transparent); color: var(--accent-ink);
}

/*
 * On a phone the bar became four ragged rows — 169px of a 844px screen before
 * any measurement, with one chip carrying a whole e-mail address across the
 * full width and "+ New" orphaned on a line of its own.
 *
 * So: the boards become one line that scrolls sideways, the way tabs do
 * everywhere else on a phone, and the controls for THIS board get their own
 * row under a rule — separated, because they act on the board that is open
 * rather than being another thing to choose.
 */
@media (max-width: 720px) {
  .boardbar { min-width: 0; }
  .boardpicks {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    min-width: 0; width: 100%;
    /* Room for the ring on the chip that is open, which a tight scroller clips. */
    padding: 2px; margin: -2px;
    -webkit-overflow-scrolling: touch;
  }
  .boardpicks::-webkit-scrollbar { display: none; }
  .boardpicks .chip { flex: 0 0 auto; max-width: 68vw; }
  /*
   * "+ New" stays put while the boards scroll under it. In a scroller it
   * would otherwise sit past the right edge with three boards on the list —
   * a button you have to go looking for is a button nobody presses. The
   * shadow is the panel's own colour, so chips disappear under it rather
   * than through it.
   */
  .boardpicks [data-newboard] {
    /* right: -2px, not 0 — the scroller carries 2px of padding so the ring on
       the open chip is not clipped, and at right:0 a sliver of the next chip
       showed in exactly that gap. */
    /* Pushed to the right edge, not left trailing the chips: with two boards
       it sat in the middle of the row with empty space beside it, which reads
       as a third board rather than as the way to make one. margin-left: auto
       takes the free space when there is any; sticky keeps it on screen when
       there is none. */
    flex: 0 0 auto; margin-left: auto; position: sticky; right: -2px;
    /* Its own primary background stays (it was overridden with --panel: accent-ink on panel, ~1.2:1, a button nobody could read). The shadow still hides the chips scrolling under it. */
    box-shadow: -12px 0 10px 10px var(--panel);
  }
}

/* The "which board" menu, hanging off the pin button that opened it. */
.pinpick {
  position: absolute; z-index: 40; margin-top: 6px; padding: 4px;
  min-width: 180px; box-shadow: 0 8px 24px rgba(0, 0, 0, .28);
}
.pinpickrow {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 10px; border: 0; border-radius: 8px; background: none;
  font: inherit; font-size: 12.5px; color: var(--fg-2); text-align: left; cursor: pointer;
}
.pinpickrow:hover { background: var(--panel-2); color: var(--fg); }
.pinpickrow .tag {
  font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase;
  padding: 1px 5px; border-radius: 4px; background: var(--panel-2); color: var(--fg-3);
}

/* ------------------------------------------------------------- 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-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; }
.hrow .evnum { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--fg); }
/* A ticket's package list (eight "name version (CVE)" lines) takes the whole
   width of the grid, not one of its halves: in a half each line broke in three. */
.hgrid > .hrow:has(> .pkglist) { grid-column: 1 / -1; }
.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%;
}
/* A structured log line (app.js logView): its message, then its fields, quieter. */
.logmsg .logk { color: var(--fg); font-weight: 600; }
.logmsg .logkv { color: var(--fg-3); margin-left: 12px; }
/* Under a trace (and in its side column): the message on a line of its own
   under time · unit · level, two lines at most, then cut. */
.tlogline { padding: 5px 0; border-top: 1px solid var(--line); }
.tlogline:first-child { border-top: 0; }
.tlogmsg { flex: 1 1 100%; white-space: normal; overflow-wrap: anywhere; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.45; }
/* What the search found, in the line it found it in. */
.loghit {
  background: var(--warn-bg);
  color: var(--fg);
  box-shadow: inset 0 -2px 0 var(--warn);
  border-radius: 2px;
  padding: 0 1px;
}

.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;
  }
}
/*
 * A strip of choices that can be longer than the screen.
 *
 * This was `overflow: hidden`, put there to clip the buttons against the
 * rounded corners — and on a phone that is not clipping, it is deletion. The
 * topology filter is one button per application, so on a narrow screen the
 * later applications were drawn, took up space, and could not be reached or
 * scrolled to by any gesture. Worse than a control that is missing: it is one
 * that exists and refuses.
 *
 * `overflow-x: auto` keeps the corner clipping — any non-visible overflow
 * clips to the radius — and gives back the swipe. The buttons stop shrinking
 * as well, because a segmented control that squeezes six names into the width
 * of two is legible to nobody.
 */
/*
 * The control is the same wherever it stands — a .pbar, a page head, a
 * settings section's heading, a form — so the rule is the control's own
 * (a segmented control always carries role=group or tablist; the waterfall's
 * bar segments, also .seg, do not).
 */
.seg:is([role=group], [role=tablist]) {
  display: inline-flex; border: 1px solid var(--line); border-radius: 7px;
  overflow-x: auto; overflow-y: hidden; max-width: 100%;
  scrollbar-width: none;
}
.seg:is([role=group], [role=tablist])::-webkit-scrollbar { height: 0; }
.seg:is([role=group], [role=tablist]) a { display: inline-block; text-decoration: none; }
.seg:is([role=group], [role=tablist]) a,
.seg:is([role=group], [role=tablist]) 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;
  flex: 0 0 auto; white-space: nowrap; }
.seg:is([role=group], [role=tablist]) a:last-child,
.seg:is([role=group], [role=tablist]) button:last-child { border-right: 0; }
.seg:is([role=group], [role=tablist]) a.on,
.seg:is([role=group], [role=tablist]) 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; }
/* A ticket's head: the page head, then its facts on one line, marked by severity. */
.pheadwrap { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.pheadwrap .pmeta { margin-top: 0; padding-left: 10px; border-left: 3px solid var(--line); }
.pheadwrap .pmeta.bad { border-left-color: var(--bad); }
.pheadwrap .pmeta.serious { border-left-color: var(--serious); }
.pheadwrap .pmeta.warn { border-left-color: var(--warn); }

/* pre-line: a memory ticket carries a numbered list of what to close, and the
   mail and the page read the same text. */
.psummary { margin: 0; max-width: 78ch; line-height: 1.6; color: var(--fg); white-space: pre-line; }
/* 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; }
/* Short rows, so as many to a line as fit: ten earlier tickets were ten full-width rows. */
.ptraces { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 4px; margin-top: 8px; }

/*
 * The problem list's three tabs, and how loudly each one speaks.
 *
 * Open now is the one that must be impossible to miss: a band in the colour of
 * the worst open ticket, a beating dot, the same colour down each open card's
 * edge and on its tab. What has ended is drawn quietly, so the two cannot be
 * confused on a glance from across the room.
 */
.ptabs button { display: inline-flex; align-items: center; gap: 6px; }
.ptabn { font-variant-numeric: tabular-nums; opacity: .75; font-weight: 600; }
.ptabs button.plive { font-weight: 700; }
.ptabs button.plive.sev-critical { color: var(--bad); }
.ptabs button.plive.sev-serious { color: var(--serious); }
.ptabs button.plive.sev-warning { color: var(--warn); }
.pdot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; flex: 0 0 9px;
  box-shadow: 0 0 0 0 currentColor; animation: pbeat 1.6s ease-out infinite; }
@keyframes pbeat {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 55%, transparent); }
  70% { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) { .pdot { animation: none; } }
.palert { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  border-left: 6px solid currentColor; font-size: 16px; padding: 14px 18px; }
.palert b { font-size: 18px; }
.palert > span:not(.pdot) { color: var(--fg); }
.palert .muted { color: var(--fg-3) !important; }
.palert.sev-critical { color: var(--bad); background: var(--bad-bg); }
.palert.sev-serious { color: var(--serious); background: var(--serious-bg); }
.palert.sev-warning { color: var(--warn); background: var(--warn-bg); }
.popen { border-left: 4px solid var(--warn); }
.popen.sev-critical { border-left-color: var(--bad); }
.popen.sev-serious { border-left-color: var(--serious); }
.pdone { opacity: .88; }
.pdone h3 { color: var(--fg-2); }
.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 > .badge { justify-self: start; } /* a badge is its word wide, also when the row stacks on a phone */
.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; }
}

/* ------------------------------------------------------ ticket timeline ---- */

/* The story of a ticket on one axis (eventTimeline in charts.js). */
.ptl { width: 100%; }
.ptl-svg { font-family: inherit; }
.ptl-tick { fill: var(--fg-3); font-size: 10.5px; font-variant-numeric: tabular-nums; }
.ptl-band { font-size: 10.5px; font-weight: 650; letter-spacing: .02em; }
.ptl-chip { fill: var(--panel); fill-opacity: .88; }
.ptl-time { font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.ptl-title { fill: var(--fg); font-size: 12px; font-weight: 600; }
.ptl-ev { cursor: default; outline: none; transition: opacity .15s ease; }
.ptl-ev.ptl-link { cursor: pointer; }
.ptl-svg.dim .ptl-ev:not(.hot) { opacity: .28; }
.ptl-svg.dim .ptl-river { opacity: .55; }
.ptl-ev.hot .ptl-stem { stroke-opacity: 1; stroke-width: 1.5; }
.ptl-ev.hot .ptl-title { text-decoration: underline; text-decoration-color: var(--line-2); text-underline-offset: 3px; }
.ptl-ev:focus-visible .ptl-target { stroke: var(--accent); stroke-width: 2; }
.ptl-river { transition: opacity .15s ease; pointer-events: none; }
.ptl-hit { cursor: crosshair; }

.ptl-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 10px; font-size: 12px; color: var(--fg-2); }
.ptl-key { display: inline-flex; align-items: center; gap: 6px; }
.ptl-key b { color: var(--fg-3); font-weight: 600; font-variant-numeric: tabular-nums; }
.ptl-key-river i { width: 16px; height: 8px; border-radius: 4px; display: inline-block; opacity: .45; }
.ptl-glyph { flex: none; overflow: visible; }

/* A paragraph, not a row of figures: the tooltip's label|value flex (below)
   would set the kind, the title and the detail side by side in three columns. */
.tooltip > div.ptl-tip { display: block; white-space: normal; max-width: 340px; line-height: 1.45; }
.tooltip .ptl-tip b { display: block; text-align: left; }
.tooltip .ptl-tip-k { color: var(--fg-3); font-size: 11px; }
.tooltip .ptl-tip-d { color: var(--fg-2); margin-top: 3px; }

.ptl-more { margin-top: 10px; }
.ptl-more > summary { cursor: pointer; font-size: 12px; color: var(--fg-3); width: max-content; }
.ptl-more > summary:hover { color: var(--fg); }
.ptl-more[open] > summary { margin-bottom: 6px; }

.ptl-list { list-style: none; margin: 0; padding: 0; position: relative; }
.ptl-list::before {
  content: ''; position: absolute; left: 7px; top: 6px; bottom: 6px; width: 1px; background: var(--line);
}
.ptl-row { position: relative; }
.ptl-rowin {
  display: grid; grid-template-columns: 14px 44px 1fr; gap: 10px; align-items: baseline;
  padding: 7px 6px 7px 0; color: inherit; text-decoration: none; border-radius: 6px;
}
a.ptl-rowin:hover { background: var(--panel-2); }
.ptl-rowin .ptl-glyph { align-self: start; margin-top: 3px; position: relative; }
.ptl-rowin time { color: var(--fg-3); font-size: 12px; font-variant-numeric: tabular-nums; }
.ptl-rowt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ptl-rowt b { font-size: 13px; font-weight: 600; color: var(--fg); overflow-wrap: anywhere; }
.ptl-rowt span { font-size: 12px; color: var(--fg-3); line-height: 1.45; overflow-wrap: anywhere; }
.ptl-day {
  padding: 10px 0 2px 24px; font-size: 11px; font-weight: 650; color: var(--fg-3);
  text-transform: uppercase; letter-spacing: .05em;
}

/* --------------------------------------------------- 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; }
/* A machine's two other identities beside its label: one click selects each, for pasting. */
.hostids { display: inline-flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.hostids > span {
  font-size: 12px; padding: 2px 8px; border-radius: 6px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--fg-2);
  user-select: all; overflow-wrap: anywhere;
}
.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; }

.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; }

/*
 * The two login cards share the narrow column, one above the other.
 *
 * .secrow is two columns on purpose, so a third card would have dropped onto
 * a line of its own at double width -- the scanning card's map above it and
 * one short table stretched across 2fr below. A machine has an auth log or a
 * Windows Security channel and almost never both, so in practice this stack
 * holds one card and costs nothing.
 */
.loginstack { display: grid; gap: 14px; align-content: start; min-width: 0; }
.loginstack > * { 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%; }

/*
 * ...except when it shares a row.
 *
 * `max-content` is right for a card standing alone: a table of three mounts
 * should not stretch to two thousand pixels of empty columns. It is wrong the
 * moment the card is in a grid — the column already decided the width, and the
 * card refusing to fill it leaves a gap exactly the size of the difference.
 * That gap is what a wide host page was mostly made of.
 */
.grid > .card.fitted,
.netpair > .card.fitted { width: auto; }

/*
 * 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; }
}

/*
 * The menu: search, two tools, the dock, and three fixed destinations.
 *
 * Every page is an app. Apps opens the launcher that holds all of them; the
 * dock holds the ones somebody opened, in the order they put them, pinned or
 * passing through. The search finds any app by its name or its group's.
 */
.navfind { padding: 0 8px 8px; }
.navfind input { padding: 6px 10px; font-size: 13px; background: var(--panel-2); }

.brand .logo { display: inline-flex; flex: none; }
.brand .logo svg { width: 22px; height: 22px; display: block; }
.brand .logo rect { fill: var(--accent); }
.brand .logo path {
  fill: none; stroke: var(--accent-ink); stroke-width: 3;
  stroke-linecap: round; stroke-linejoin: round;
}

.navtools { padding: 8px 8px 6px; display: flex; flex-direction: column; gap: 2px; }
.navtool {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 6px 9px; border: 0; border-radius: 7px; background: none;
  color: var(--fg-2); font-weight: 500; text-align: left; cursor: pointer;
}
.navtool:hover, .navtool[aria-expanded="true"] { background: var(--panel-2); color: var(--fg); }

/* An app's mark: two letters, or a glyph, on its group's colour. */
.appicon {
  --g: var(--fg-3);
  display: inline-grid; place-items: center; flex: none;
  width: 22px; height: 22px; border-radius: 6px;
  background: color-mix(in srgb, var(--g) 16%, transparent); color: var(--g);
  font-size: 10px; font-weight: 700; letter-spacing: .02em; line-height: 1;
}
.appicon::before { content: attr(data-mono); }
.appicon svg { width: 13px; height: 13px; display: block; }
.g-frontend { --g: var(--c-app); }
.g-marketing { --g: var(--c-domain); }
.g-automation { --g: var(--s2); }
.g-business { --g: var(--info); }
.g-analysis { --g: var(--s1); }

/* ---- trend analysis ---- */
.trends { display: grid; gap: 14px; }
.trends > .card { min-width: 0; }
.trendctl { display: grid; gap: 10px; }
.trendrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
.trendrow .chips { display: flex; flex-wrap: wrap; gap: 6px; }
.trendlab { font-size: 12px; color: var(--fg-3); min-width: 112px; }
.trendpick .f { min-width: 170px; flex: 1 1 170px; max-width: 340px; }
.trendpick select { width: 100%; }
.trendday .row { gap: 6px; flex-wrap: nowrap; }
.trendday input[type=date] { min-width: 0; flex: 1; }
.trendlegend { margin-top: 8px; }
.trends.busy > .card:not(.trendctl) { opacity: .45; transition: opacity .2s; }
.trends.busy .trendctl::after {
  content: 'Reading…'; font-size: 12px; color: var(--fg-3);
}
@media (max-width: 640px) {
  .trendlab { min-width: 0; width: 100%; }
  .trendpick .f { max-width: none; }
}
/*
 * A wide screen gets two columns. One column stretched every chart to 1800
 * pixels of a day that is 96 buckets wide — a flat ribbon — and pushed the
 * day-by-day view and the table off the bottom. The day sits beside the days,
 * the difference beside where it left the range; the controls, the numbers
 * and the table keep the full width.
 */
@media (min-width: 1200px) {
  .trends { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
  .trends > .trendctl, .trends > .trendkpis, .trends > .tr-table, .trends > .trendnone, .trends > .empty { grid-column: 1 / -1; }
  .trends > .tr-day { order: 1; }
  .trends > .tr-daily { order: 2; }
  .trends > .tr-diff { order: 3; }
  .trends > .tr-out { order: 4; }
  .trends > .tr-table { order: 5; }
  /* The three choices about what is laid over the day, side by side. */
  .trendctl { grid-template-columns: repeat(3, auto); justify-content: start; column-gap: 28px; }
  .trendctl > .trendrow:first-child, .trendctl > .trendpick { grid-column: 1 / -1; }
  .trendctl .trendlab { min-width: 0; width: 100%; }
  .trendctl > .trendrow:not(:first-child) { flex-direction: column; align-items: flex-start; }
}
.g-profiling { --g: var(--serious); }
.g-backend { --g: var(--c-host); }
.g-install { --g: var(--accent); }
.g-data { --g: var(--c-service); }
.g-iotg { --g: var(--s3); }
.g-integ, .g-ai { --g: var(--ai); }
.g-sec { --g: var(--bad); }
.g-apps { --g: var(--fg-2); }

/* ---- the dock ---- */
.nav.dock { padding: 8px 8px 10px; border-top: 1px solid var(--line); }
/*
 * The grip and the pin lie over the row rather than beside it, so the
 * link — and its highlight — runs the full width, and the icons line up with
 * AI assist and Apps above. A pinned row keeps room for the pin it shows.
 */
.dockrow { position: relative; display: flex; align-items: center; border-radius: 7px; margin-bottom: 2px; }
.dockrow a, .navresults a {
  flex: 1; min-width: 0; gap: 10px; padding: 6px 9px; margin: 0;
}
.dockrow.pinned a { padding-right: 32px; }
.dockrow:hover a, .dockrow:focus-within a { padding-right: 32px; }
.dockrow .dockgrip { position: absolute; left: -7px; top: 50%; transform: translateY(-50%); width: 10px; }
.dockacts { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); display: flex; gap: 2px; }
.dl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dl small { display: block; font-size: 11px; font-weight: 400; color: var(--fg-3); }
.dockgrip {
  flex: none; display: grid; place-items: center; width: 14px; height: 28px;
  padding: 0; border: 0; border-radius: 4px; background: none;
  color: var(--fg-3); cursor: grab; touch-action: none; opacity: 0;
}
.dockgrip svg { width: 6px; height: 11px; display: block; }
.dockbtn {
  flex: none; display: grid; place-items: center; width: 24px; height: 24px;
  padding: 0; border: 0; border-radius: 6px; background: none;
  color: var(--fg-3); font-size: 12px; line-height: 1; cursor: pointer; opacity: 0;
}
.dockbtn svg { width: 13px; height: 13px; display: block; fill: none; }
.dockbtn[aria-pressed="true"] svg { fill: currentColor; }
.dockbtn:hover { background: var(--panel-2); color: var(--fg); }
.dockrow:hover .dockgrip, .dockrow:hover .dockbtn,
.dockrow:focus-within .dockgrip, .dockrow:focus-within .dockbtn { opacity: 1; }
/* A pinned entry shows its pin: that is how the dock says which ones stay. */
.dockrow.pinned .dockpin { opacity: 1; color: var(--accent); }
.dockrow.dragging { background: var(--panel-2); box-shadow: var(--shadow); }
.dockrow.dragging .dockgrip { cursor: grabbing; }
.navresults .navlocked { gap: 9px; padding: 6px 8px; }
@media (hover: none) { .dockgrip, .dockbtn { opacity: .7; } }
/*
 * Start here: while nothing reports, Install is drawn the way the primary
 * button is, with a slow pulse beside its name. It goes back to an ordinary
 * row the moment anything is connected.
 */
.dockrow.start a, .dockrow.start a:hover, .dockrow.start a.active {
  background: var(--accent); color: var(--accent-ink); font-weight: 600; box-shadow: none;
}
.dockrow.start a:hover { background: color-mix(in srgb, var(--accent) 88%, var(--fg)); }
.dockrow.start .appicon { --g: var(--accent-ink); }
.dockrow.start .dockbtn, .dockrow.start.pinned .dockpin { color: var(--accent-ink); }
.dockrow.start .dockbtn:hover { background: color-mix(in srgb, var(--accent-ink) 18%, transparent); }
.startdot {
  flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-ink);
  animation: startpulse 2s ease-out infinite;
}
@keyframes startpulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-ink) 60%, transparent); }
  70%, 100% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent-ink) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .startdot { animation: none; } }

/* ---- the bottom three ---- */
.navtopo.navsettings { border-color: var(--line); background: transparent; color: var(--fg-2); }
.navtopo.navsettings:hover { background: var(--panel-2); color: var(--fg); }
.navtopo.navsettings.active { background: var(--panel-2); color: var(--fg); }
.navsettings .gear { width: 7px; margin-right: 9px; font-size: 12px; line-height: 1; display: inline-grid; place-items: center; }

/* ---- the launcher ---- */
/*
 * Beside the menu, over the page: it is a place you pass through on the way
 * to an app, so it covers what you were reading rather than pushing it aside.
 * The sidebar is lifted above the sticky topbar while it is open, because
 * the launcher lives inside it.
 */
:root.launcher-open .sidebar { z-index: 55; }
.launcher-scrim {
  position: fixed; top: 0; bottom: 0; right: 0; left: 236px; z-index: 1;
  background: color-mix(in srgb, black 30%, transparent);
}
.launcher {
  position: fixed; top: 0; bottom: 0; left: 236px; z-index: 2;
  width: min(820px, calc(100vw - 236px));
  display: flex; flex-direction: column;
  background: var(--panel); border-right: 1px solid var(--line); box-shadow: var(--shadow-lg);
}
.lhead {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 16px 13px 20px; border-bottom: 1px solid var(--line);
}
.lhead b { font-size: 16px; letter-spacing: -.01em; }
.lhead input { flex: 1; max-width: 320px; margin-left: auto; padding: 6px 10px; font-size: 13px; background: var(--panel-2); }
.lbody { flex: 1; overflow-y: auto; padding: 4px 20px 24px; }
.launcher .sec {
  margin: 18px 0 8px; font-size: 11px; text-transform: uppercase;
  letter-spacing: .08em; color: var(--fg-3); font-weight: 600;
}
.launcher .navitems { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 6px; }
.ltile { position: relative; min-width: 0; }
.ltile a, .ltile .navlocked {
  display: flex; align-items: center; gap: 10px; min-width: 0;
  padding: 9px 34px 9px 10px; border: 1px solid var(--line); border-radius: 9px;
  background: var(--panel); color: var(--fg); font-weight: 500;
}
.ltile a:hover { border-color: var(--line-2); background: var(--panel-2); text-decoration: none; }
.ltile a.active { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); background: var(--accent-bg); }
.ltile .tl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ltile .appicon { width: 28px; height: 28px; border-radius: 8px; font-size: 11px; }
.ltile.locked .navlocked { color: var(--fg-3); opacity: .6; cursor: not-allowed; user-select: none; padding-right: 10px; }
.ltpin { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); }
.ltile:hover .ltpin, .ltpin:focus-visible, .ltpin[aria-pressed="true"] { opacity: 1; }
.ltpin[aria-pressed="true"] { color: var(--accent); }
@media (hover: none) { .ltpin { opacity: .7; } }

/*
 * The assistant has a place in the menu now, so the corner mark steps aside
 * wherever the menu is on screen. On a phone (a drawer) and with the menu
 * folded away it is the only way in, so it stays.
 */
@media (min-width: 801px) {
  body:not(.nav-folded) .assistantbtn { display: none; }
}
/* And while the drawer is open on a phone, the menu's own AI assist is right there. */
:root.drawer-open .assistantbtn { display: none; }

@media (max-width: 800px) {
  /* Inside the drawer, which is transformed, so fixed means "the drawer": widen it. */
  :root.launcher-open .sidebar { width: 100vw; flex-basis: 100vw; border-right: 0; }
  .launcher { left: 0; width: 100%; border-right: 0; box-shadow: none; }
  .launcher-scrim { display: none; }
  .lhead { padding: 10px 10px 10px 14px; }
  .lhead input { font-size: 16px; }
  .lbody { padding: 0 14px 20px; }
  .launcher .navitems { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}

/*
 * 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; }
/* Scoped: a bare .hrow here overrode the name/value rows of the same name
   (".hrow" under "Headers" above — a trace's headers, a ticket's "The
   measurement") with four columns, which stood a ticket's package list in a
   narrow third column 900px tall. */
.hmeters .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) {
  .hmeters .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; }
}

/* ------------------------------------------------ topology: the one card ---- */

/*
 * One card for the whole map: a header bar, then the two pictures.
 *
 * The page used to stack three boxes before the picture — a strip of one
 * button per application that ran off the edge at nineteen, four full-size
 * number cards, and a card holding a toggle and a search. The map began half
 * a screen down. The bar holds all of it in two rows at most.
 *
 * Laid out against the CARD's width (a container query), not the window's:
 * the menu folding changes the room the card has without the window changing
 * at all.
 */
.topocard { container: topo / inline-size; }
.topohead {
  display: grid; gap: 10px 16px; align-items: center;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "pick" "stats" "tools";
  padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid var(--line);
}
.topopick { grid-area: pick; min-width: 0; }
.topopick:empty { display: none; }
.topostats { grid-area: stats; min-width: 0; }
.topotools { grid-area: tools; min-width: 0; display: flex; align-items: center; gap: 10px; }
@container topo (min-width: 620px) {
  .topohead {
    grid-template-columns: minmax(180px, 260px) minmax(0, 1fr);
    grid-template-areas: "pick tools" "stats stats";
  }
  .topohead:has(.topopick:empty) { grid-template-areas: "tools tools" "stats stats"; }
}
@container topo (min-width: 1180px) {
  .topohead {
    grid-template-columns: minmax(180px, 240px) minmax(0, 1fr) minmax(300px, 400px);
    grid-template-areas: "pick stats tools";
  }
  .topohead:has(.topopick:empty) { grid-template-areas: "stats stats tools"; }
}

/*
 * The four numbers as a strip: a figure and its noun on one line, the detail
 * under it. Dividers rather than boxes — they are one statement about one map,
 * not four cards. Two by two on a narrow card.
 */
.topostats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
@container topo (min-width: 620px) {
  .topostats { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; }
  .tstat + .tstat { border-left: 1px solid var(--line); border-radius: 0; }
}
.tstat {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
  padding: 4px 12px; border-radius: 7px; text-align: left;
  background: none; border: 0; color: var(--fg-2); font: inherit; text-decoration: none;
}
@container topo (max-width: 619px) {
  .tstat { background: var(--panel-2); border: 1px solid var(--line); padding: 6px 10px; }
}
.tsv { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tsv b { font-size: 17px; font-weight: 700; color: var(--fg); margin-right: 2px; font-variant-numeric: tabular-nums; }
.tsv b.bad { color: var(--bad); }
.tsgo { color: var(--accent); }
.tss { font-size: 11.5px; color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.tstat, button.tstat { cursor: pointer; transition: background .12s; }
a.tstat:hover, button.tstat:hover { background: var(--panel-2); text-decoration: none; }
a.tstat:focus-visible, button.tstat:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Idle as a switch, then the search, which takes what is left. */
.tidle {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  background: none; border: 0; padding: 4px 2px; cursor: pointer;
  color: var(--fg-2); font: inherit; font-size: 12.5px; white-space: nowrap;
}
.tidle .switch { display: inline-block; flex: none; }
.tidle:hover .switch { border-color: var(--accent); }
.tidle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.topofind { position: relative; flex: 1 1 auto; min-width: 0; display: flex; align-items: center; }
.topofind input[type="search"] { padding: 6px 10px; font-size: 13px; }
.topofind input[type="search"]:not(:placeholder-shown) { padding-right: 76px; }
.topofind #tfound {
  position: absolute; right: 30px; pointer-events: none; white-space: nowrap; font-size: 11.5px;
}

/*
 * The application picker: a button the size of one name, and a list under it
 * with its own filter. The panel speaks the user menu's language.
 */
.tpick { position: relative; min-width: 0; }
.tpick-btn {
  display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;
  background: var(--panel-2); color: var(--fg); border: 1px solid var(--line-2); border-radius: 7px;
  padding: 6px 10px; font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  transition: border-color .12s;
}
.tpick-btn:hover, .tpick-btn[aria-expanded="true"] { border-color: var(--accent); }
.tpick-btn:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 1px; }
.tpick-name { flex: 1 1 auto; min-width: 0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tpick-n {
  flex: none; font-size: 11px; font-weight: 600; color: var(--fg-3);
  background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 0 7px;
}
.tpick-caret { flex: none; color: var(--fg-3); font-size: 11px; }
.tpick-ico { flex: none; display: inline-flex; width: 15px; height: 15px; color: var(--fg-3); }
.tpick-ico svg { width: 15px; height: 15px; }
.tpick-ico.k-all { color: var(--accent); }
.tpick-ico.k-mobile { color: var(--c-service); }
.tpick-pop {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;
  width: max(100%, 280px); max-width: min(360px, calc(100vw - 32px)); padding: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: var(--shadow-lg);
}
.tpick-q { padding: 6px 10px; font-size: 13px; margin-bottom: 6px; }
.tpick-list { list-style: none; margin: 0; padding: 0; max-height: min(340px, 52vh); overflow-y: auto; }
.tpick-list li {
  display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 7px;
  font-size: 13px; color: var(--fg-2); cursor: pointer;
}
.tpick-list li .nm { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tpick-list li.act { background: var(--panel-2); color: var(--fg); }
.tpick-list li.on { color: var(--accent); font-weight: 600; }
.tpick-list li.on::after { content: '✓'; margin-left: auto; padding-left: 8px; }
.tpick-none { padding: 8px 10px; }

/*
 * The two pictures: one large, one a live miniature beside it.
 *
 * The miniature is the large render itself, laid out at the large width and
 * scaled by 24/76 — the ratio of the two grid columns — so it is the same
 * picture, search marks and all, not a thumbnail of it. It is inert: nothing
 * inside it can be pointed at or tabbed to, and the whole of it is one
 * control that swaps it in. On a narrow card the two simply stack, the chosen
 * one first, and nothing swaps.
 */
.topostage { display: flex; flex-direction: column; gap: 14px; }
.topostage .tview.mini { order: 1; }
.tview { min-width: 0; display: flex; flex-direction: column; }
.tvhead {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 10px; font-size: 13px; font-weight: 650; color: var(--fg-2);
}
.tvname { color: var(--fg); }
.tvhead .tp-legend { margin-left: auto; font-weight: 400; }
.tvgo { display: none; }
/* Set by the page from the measured miniature (see fitMini); these are the
   values it starts from. */
.tvframe { min-width: 0; --mini-h: 160px; --tv-s: .3; }
@container topo (min-width: 760px) {
  .topostage {
    display: grid; grid-template-columns: minmax(0, 76fr) minmax(0, 24fr);
    align-items: start;
  }
  .tview.big { grid-column: 1; grid-row: 1; }
  .tview.mini {
    grid-column: 2; grid-row: 1;
    position: sticky; top: 72px;
    padding: 8px; border: 1px solid var(--line); border-radius: 10px;
    background: var(--panel-2); cursor: pointer;
    transition: border-color .15s, box-shadow .15s;
  }
  .tview.mini:hover { border-color: var(--accent); box-shadow: var(--shadow-lg); }
  .tview.mini:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .tview.mini .tvhead { margin-bottom: 6px; font-size: 12px; }
  .tview.mini .tvhead .drill, .tview.mini .tvhead .tp-legend { display: none; }
  .tview.mini .tvgo { display: inline; margin-left: auto; color: var(--fg-3); font-size: 13px; }
  .tview.mini:hover .tvgo { color: var(--accent); }
  .tview.mini .tvframe {
    height: var(--mini-h); overflow: hidden; border-radius: 6px;
    background: var(--bg); border: 1px solid var(--line);
  }
  /* The large width, scaled to the small one. The scale is measured by the
     page (--tv-s): the miniature's padding and borders make it a ratio of
     two lengths, which CSS cannot divide. */
  .tview.mini .tvbody {
    width: calc((100cqw - 14px) * .76);
    transform: scale(var(--tv-s));
    transform-origin: 0 0; pointer-events: none;
  }
  .tview.mini .tvbody .scape { overflow: hidden; }
  .topostage.swapping .tview { animation: tvswap .24s ease-out; }
}
@keyframes tvswap { from { opacity: .3; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .topostage.swapping .tview { animation: none; } }

/* 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; }

/*
 * The two lines a person draws for "slow", and the bands they make.
 *
 * A slider whose number lives somewhere else is a slider nobody trusts, so
 * the value sits beside it — and the bar above is the whole rule in one
 * glance: green up to the first line, amber to the second, red past it.
 */
.slowbar { display: flex; height: 8px; border-radius: 999px; overflow: hidden; margin-bottom: 10px; }
.slowbar > span { display: block; }
.sb-good { background: var(--good); }
.sb-warn { background: var(--warn); }
.sb-crit { background: var(--bad); }
.slowrow { display: grid; grid-template-columns: 110px 1fr 64px; align-items: center; gap: 10px; }
.slowrow > label { margin: 0; font-size: 12px; color: var(--fg-2); }
.slowrow > output { text-align: right; font-size: 12px; color: var(--fg); }
.slowrow > input[type=range] { width: 100%; accent-color: var(--accent); }
@media (max-width: 620px) { .slowrow { grid-template-columns: 92px 1fr 56px; gap: 6px; } }

/* The password form, under the row it belongs to — never floating free. */
.pwform {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end;
  padding: 10px; margin: 4px 0; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line);
}

/*
 * The values an application works with, one tile each.
 *
 * How a tile is drawn is decided by the values in it, not by its name: a name
 * whose every reading was a number gets the shape of the readings, anything
 * else gets what it has been and how often.
 */
.bizgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.biztile { padding: 12px; margin: 0; }
.bizhead { display: flex; align-items: baseline; gap: 8px; }
.bizhead > b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.bizval {
  font-size: 22px; font-weight: 650; letter-spacing: -.02em; color: var(--fg);
  margin: 6px 0 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bizmeta { margin-bottom: 8px; }
.bizbody { min-height: 44px; }
.bizrow { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
.bizrowv {
  font-size: 11px; color: var(--fg-2); max-width: 46%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bizrow .pbtrack { flex: 1; }
.bizmore {
  font: inherit; font-size: 11px; color: var(--fg-3); background: transparent; border: 0;
  padding: 6px 0 0; cursor: pointer; text-decoration: underline dotted;
}
.bizmore:hover { color: var(--fg-2); }
.bizrecent { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 4px; }

/*
 * Who, before why.
 *
 * A ticket is read in a hurry, and the first question is always what to look
 * at. The detector's paragraph explains the fault; this line names the thing,
 * and it sits above the prose rather than in its last clause.
 */
.blame {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
  margin: 0 0 10px; padding: 9px 12px; border-radius: 10px;
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
}
.blamek { font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-3); }
.blame > b { font-size: 15px; color: var(--fg); }
.blamew { color: var(--fg-2); font-size: 12.5px; }
.blames {
  margin-left: auto; font-size: 12.5px; font-weight: 600; color: var(--warn);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 620px) { .blames { margin-left: 0; } }

/*
 * An SSH login from a new place: where, in words, first and large — the one
 * thing the reader is asking ("was that me?"). The serious tint, because a
 * login nobody expected is the one ticket that may mean somebody else is in.
 */
.pplace {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  margin: 0 0 12px; padding: 10px 14px; border-radius: 10px;
  background: color-mix(in srgb, var(--serious) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--serious) 40%, transparent);
}
.pplace > b { font-size: 20px; line-height: 1.25; color: var(--fg); }
.pplace .blamew { flex-basis: 100%; }

/* The policy text: it is prose, so it wraps and keeps its paragraphs. */
.snippet.policy {
  white-space: pre-wrap; max-height: 420px; overflow: auto; line-height: 1.5;
}

/* Steps, in order, on an install page — the shortest form that still reads. */
.steps { margin: 0 0 10px; padding-left: 20px; }
.steps > li { margin: 3px 0; color: var(--fg-2); font-size: 12.5px; line-height: 1.5; }

}

/*
 * Who may see what, ticked rather than typed.
 *
 * Its own card, not a form inside a table cell: a cell's alignment, the
 * table's column widths and the label element's own display all fought it,
 * and what arrived on screen was a checkbox on one side and a truncated name
 * on the other. Everything below is stated outright for the same reason.
 */
.scopecard { margin-top: 12px; }
.scopeadmin {
  display: flex; align-items: flex-start; gap: 9px; text-align: left;
  margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line);
  font-size: 13px; color: var(--fg-2); font-weight: 400; cursor: pointer;
}
.scopeadmin input { margin-top: 2px; flex: none; }
.scopeadmin b { color: var(--fg); font-weight: 600; }
.scopelists {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px;
}
/* Its [hidden] (the Administrator tick hides the lists) is the global rule at the top. */
.scopegroup {
  min-width: 0; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
}
.scopegroup > .k {
  display: block; margin: 0 0 6px; font-size: 10.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--fg-3);
}
.scopepicks { max-height: 260px; overflow-y: auto; }
.scopepick {
  display: flex; align-items: flex-start; gap: 8px; text-align: left;
  margin: 0; padding: 4px 0; font-size: 12.5px; font-weight: 400;
  color: var(--fg-2); cursor: pointer; line-height: 1.4;
}
.scopepick:hover { color: var(--fg); }
.scopepick input { margin-top: 2px; flex: none; }
.scopepick span { min-width: 0; overflow-wrap: anywhere; }
.scopefoot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 14px; }

/*
 * What the account may DO sits above the entity lists and across the whole
 * card: it is one list, not one of three columns, and its rows carry a line
 * of explanation each — a tick called "install" means nothing to somebody
 * deciding whether to give it away.
 */
.scopegroup.wide { grid-column: 1 / -1; }
.scopegroup.wide .scopepicks {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 4px 16px; max-height: none;
}
.scopepick.cap { padding: 6px 0; }
.scopepick.cap b { color: var(--fg); font-weight: 600; }
.scopepick.cap .small { display: block; line-height: 1.35; margin-top: 1px; }

/*
 * The checkboxes get a size of their own.
 *
 * The form styles in this file give every input `width: 100%`, which is right
 * for the text fields it was written for and turns a checkbox into a
 * full-width bar that pushes its own label off the card — which is exactly
 * what it did here, and what a screenshot showed and jsdom could not.
 */
.scopeadmin input[type=checkbox],
.scopepick input[type=checkbox] { margin: 2px 0 0; }

/* A refusal is an answer: it gets a card, not a spinner that never stops. */
.denied { text-align: left; border-left: 3px solid var(--warn); }
.denied b { color: var(--fg); font-size: 14px; }

/*
 * The load test's verdict card.
 *
 * One colour, one word, one sentence. It replaced a table of three limits and
 * a thread count that a reader had to do arithmetic on — and that did not move
 * while they adjusted the test, because it described the machine while the
 * form described the plan. The whole point of this card is that it changes as
 * you type, so it is loud enough to notice when it does.
 *
 * The tint comes from the same tokens as every other state in the product, at
 * low alpha over the card background, so it reads in both themes without a
 * second palette.
 */
.fit { border-left: 4px solid var(--fg-3); }
.fit-good { border-left-color: var(--good); background: color-mix(in srgb, var(--good) 7%, var(--panel)); }
.fit-warn { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, var(--panel)); }
.fit-bad  { border-left-color: var(--bad);  background: color-mix(in srgb, var(--bad) 8%, var(--panel)); }
.fit-info { border-left-color: var(--info); background: color-mix(in srgb, var(--info) 7%, var(--panel)); }
.fitmark {
  width: 34px; height: 34px; flex: 0 0 34px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; font-size: 18px; font-weight: 700; line-height: 1;
}
.fitmark.good { background: var(--good); color: var(--ink-invert); }
.fitmark.warn { background: var(--warn); color: var(--ink-invert); }
.fitmark.bad  { background: var(--bad);  color: var(--ink-invert); }
.fitmark.info { background: var(--info); color: var(--ink-invert); }
.fitsays { font-size: 16px; font-weight: 600; }
/*
 * The two numbers, right-aligned and small. They are the evidence for the
 * sentence beside them and must never compete with it — the first version of
 * this card led with them and that is exactly what made it unreadable.
 */
.fitnums { text-align: right; white-space: nowrap; }
.fitnums b { font-size: 15px; }
@media (max-width: 560px) { .fitnums { display: none; } }

/*
 * The load test's sliders.
 *
 * A native range input styled down to a track and a thumb, because the one
 * thing that must not be rebuilt here is the dragging: a div with pointer
 * events loses keyboard control, focus, touch and every screen reader, and a
 * load test is a control somebody may well drive from a phone.
 *
 * The real input sits transparent over the painted track, so what a person
 * touches is the browser's own control and what they see is ours.
 */
.sld { margin: 0 0 14px; }
.sldhead { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.sldlabel { font-size: 12px; color: var(--fg-3); }
.sldval {
  margin-left: auto; font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--fg);
}
.sldtrack {
  position: relative; height: 22px; display: flex; align-items: center;
}
.sldtrack::before {
  content: ''; position: absolute; left: 0; right: 0; height: 6px;
  border-radius: 3px; background: var(--panel-2); border: 1px solid var(--line);
}
.sldfill {
  position: absolute; left: 0; height: 6px; border-radius: 3px;
  background: var(--s1); pointer-events: none;
}
.sld.over .sldfill { background: var(--bad); }
.sld.over .sldval { color: var(--bad); }
/*
 * The machine's ceiling, on the track itself. This is the number somebody
 * needs while they are choosing, not after — a verdict card underneath is one
 * step too late.
 */
.sldcap {
  position: absolute; top: 2px; width: 2px; height: 18px;
  background: var(--warn); pointer-events: none;
}
.sldcap::after {
  content: ''; position: absolute; left: -3px; top: -3px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--warn);
}
.sldtrack input[type=range] {
  position: relative; width: 100%; margin: 0; height: 22px;
  background: transparent; -webkit-appearance: none; appearance: none;
}
.sldtrack input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--panel); border: 2px solid var(--s1); cursor: grab;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}
.sldtrack input[type=range]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--panel); border: 2px solid var(--s1); cursor: grab;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}
.sld.over .sldtrack input[type=range]::-webkit-slider-thumb { border-color: var(--bad); }
.sld.over .sldtrack input[type=range]::-moz-range-thumb { border-color: var(--bad); }
.sldtrack input[type=range]:focus-visible { outline: 2px solid var(--s1); outline-offset: 3px; }
/* Three across on a desk, one under the other on a phone. */
.sldrow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
@media (max-width: 700px) { .sldrow { grid-template-columns: 1fr; } }
/* While a run is going the sliders shape the next one, and look it. */
.sldrow.sldoff { opacity: 0.45; }
.sldrow.sldoff input[type=range] { cursor: not-allowed; }

/* The run that is going: where it is, and what the site is doing about it. */
.ltlive { margin: 14px 0 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: 10px; }
.ltbar { height: 6px; margin: 8px 0 0; border-radius: 3px; background: var(--line); overflow: hidden; }
.ltbar > div { height: 100%; background: var(--warn); transition: width 0.6s linear; }

/*
 * The question mark, and what is behind it.
 *
 * A page that explains itself in paragraphs is a page somebody has to read
 * before they can use it. The load test's prose is all still here — one click
 * away instead of under every control.
 */
.hintwrap { position: relative; display: inline-block; }
.hintbtn {
  width: 15px; height: 15px; padding: 0; border-radius: 50%;
  border: 1px solid var(--line-2); background: transparent; color: var(--fg-3);
  font-size: 10px; font-weight: 700; line-height: 1; cursor: help;
  vertical-align: middle;
}
.hintbtn:hover, .hintbtn[aria-expanded="true"] {
  border-color: var(--s1); color: var(--s1);
}
.hintbtn:focus-visible { outline: 2px solid var(--s1); outline-offset: 2px; }
.hintpop {
  position: absolute; z-index: 30; left: 0; top: 20px; width: 300px; max-width: 76vw;
  padding: 9px 11px; border-radius: 6px;
  background: var(--panel); border: 1px solid var(--line-2);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .28);
  font-size: 12px; font-weight: 400; line-height: 1.45; color: var(--fg-2);
  white-space: normal; text-align: left;
}
/* Near the right edge it opens leftwards instead of off the screen. */
.hintwrap:nth-last-child(-n+2) .hintpop { left: auto; right: 0; }

/*
 * The playhead, which the timeline did not have.
 *
 * Position was shown only by the edge of the filled bar, and the red ticks
 * beside it are event markers — an error at the start of a recording draws one
 * at the far left. Read together, the red tick looked like a position
 * indicator that had stopped moving while the blue bar advanced past it, and
 * was reported as exactly that.
 *
 * So the position gets its own mark: a knob on the fill's edge, in the same
 * colour as the fill, which cannot be confused with the severity colours the
 * markers use. It sits above the markers so it is never hidden by one.
 */
.replay-head {
  position: absolute; top: 2px; width: 4px; height: 18px; margin-left: -2px;
  background: var(--accent); border-radius: 2px; pointer-events: none; z-index: 2;
  box-shadow: 0 0 0 2px var(--panel);
}

/*
 * The parameter rows on a device page.
 *
 * A name and a value, and the value gets the room: names are short by rule
 * (a C identifier) and values are the part somebody reads back to check. On a
 * phone the pair stacks rather than shrinking both into uselessness.
 */
.paramed .paramrow { align-items: center; }
.paramed .paramrow .pval { flex: 1 1 auto; min-width: 0; }
.paramed .paramrow .pname { flex: 0 0 auto; }
@media (max-width: 640px) {
  .paramed .paramrow { flex-wrap: wrap; }
  .paramed .paramrow .pname { max-width: none !important; flex: 1 1 100%; }
}

/*
 * A device's own log lines.
 *
 * Not the Logs page's rows: those carry a host and a unit and are filtered by
 * both. These are one board talking about itself, read beside the gap they
 * explain — so three columns, a fixed width for the two short ones, and the
 * message getting everything that is left.
 */
.loglines { max-height: 340px; overflow-y: auto; }
.logline {
  display: grid; grid-template-columns: 62px 46px 1fr; gap: 8px; align-items: baseline;
  padding: 3px 0; border-top: 1px solid var(--line);
}
.logline:first-child { border-top: 0; }
.logline .lvl { font-size: 10px; text-transform: uppercase; color: var(--fg-3); letter-spacing: .04em; }
.logline .lvl.warn { color: var(--warn); }
.logline .lvl.bad { color: var(--bad); }
@media (max-width: 640px) {
  .logline { grid-template-columns: 58px 40px 1fr; gap: 6px; }
}

/*
 * A third line under a fact, for the thing that qualifies it.
 *
 * "192.168.1.42" and "via 78.92.243.54" are one fact, not two: the second only
 * means anything under the first. Same for a signal under a network name and a
 * boot count under an uptime — as separate facts they would double the grid
 * and halve the meaning.
 */
.facts .sub { display: block; margin-top: 1px; color: var(--fg-3); }

/*
 * A folded card's summary, with its own buttons on it.
 *
 * The reference material on an install page — a sixty-line sketch, a brief
 * written for somebody else — is the bulk of the page and almost never what
 * the reader came for. Folded it is one line. The buttons stay on that line
 * because the common case is copying the sketch rather than reading it, and
 * expanding sixty lines to reach a copy button is a step that exists for no
 * reason.
 */
details.card > summary .spacer { flex: 1; }
details.card > summary .foldbtns { flex: 0 0 auto; }
details.card > summary { flex-wrap: wrap; row-gap: 6px; }

/* A page this account was not given: shown, greyed, inert, with the reason on hover. */
.nav .navlocked, .sidebar-foot .navlocked {
  display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
  padding: 8px 11px; margin-bottom: 2px; border-radius: 7px;
  color: var(--fg-3); font-weight: 500; opacity: .6; cursor: not-allowed; user-select: none;
}
.navlock { margin-left: auto; font-size: 11px; filter: grayscale(1); }

/* A capability or a list that does nothing right now, because what it lives inside is off. */
.scopedim > span:not([data-backend-note]), .scopegroup.scopedim .scopepicks { opacity: .5; }
[data-needs-note], [data-backend-note] { display: block; color: var(--warn); margin-top: 2px; }

/*
 * A Sentry person (sentryUserView in app.js): who, then their events on the
 * left and the one chosen on the right — one open at a time, the way a mail
 * client reads. Below 900px the list sits above the event.
 */
.seperson { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
.seavatar { width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; flex: 0 0 auto;
  background: var(--accent-bg); color: var(--accent); font-weight: 700; font-size: 18px; }
.sestats { display: flex; gap: 22px; flex-wrap: wrap; }
.sestats > div { display: flex; flex-direction: column; min-width: 0; }
.sestats b { font-size: 15px; }
.sestats b.bad { color: var(--bad); }
.sestats span { font-size: 11px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .06em; }
.sesplit { display: grid; grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); gap: 14px; align-items: start; }
.selist { padding: 6px; max-height: calc(100vh - 250px); overflow-y: auto; position: sticky; top: 12px; }
.seday { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); font-weight: 600; padding: 10px 8px 4px; }
.seitem { display: flex; gap: 9px; width: 100%; text-align: left; background: none; border: 0; border-radius: 7px;
  padding: 8px; cursor: pointer; color: var(--fg); font: inherit; align-items: flex-start; }
.seitem:hover { background: var(--panel-2); }
.seitem.on { background: var(--accent-bg); box-shadow: inset 2px 0 0 var(--accent); }
.seitem-b { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.seitem-top { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }
.seitem-title { color: var(--fg-2); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow-wrap: anywhere; }
.sedot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; margin-top: 6px; background: var(--warn); }
.sedot.bad { background: var(--bad); }
.sedetail { min-width: 0; }
.sehead-t { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.sehead-t { flex-wrap: nowrap; align-items: flex-start; }
.sehead-t h3 { margin: 0; font-size: 17px; overflow-wrap: anywhere; flex: 0 1 auto; min-width: 0; }
.sehead-t .sedot { margin-top: 8px; }
.sehead-t .badge { margin-top: 2px; flex: 0 0 auto; }
.sehead-t .sedot { margin-top: 0; }
.sehead .muted { margin-top: 4px; }
.semsg { margin: 12px 0; padding: 10px 12px; border-radius: 8px; background: var(--panel-2); border-left: 3px solid var(--bad);
  font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.semsg.long > span { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.linkbtn { background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer; font: inherit; }
.linkbtn:hover { text-decoration: underline; }
.sefacts { display: flex; flex-wrap: wrap; gap: 6px; }
.sefact { font-size: 12px; padding: 3px 9px; border-radius: 20px; background: var(--panel-2); border: 1px solid var(--line); color: var(--fg-2); }
.sefilters { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 10px; }
.sefilter { font: inherit; font-size: 12px; padding: 3px 10px; border-radius: 20px; cursor: pointer;
  background: var(--panel); border: 1px solid var(--line); color: var(--fg-3); display: inline-flex; gap: 5px; align-items: center; }
.sefilter.on { color: var(--fg); border-color: var(--line-2); background: var(--panel-2); }
.sefilter:not(.on) .setl-ic { opacity: .4; }
.setl { list-style: none; margin: 0; padding: 0; max-height: 520px; overflow-y: auto; position: relative; }
.setl-i { display: grid; grid-template-columns: 58px 24px minmax(0, 1fr); gap: 8px; align-items: start; padding: 5px 6px; border-radius: 6px; }
.setl-i:hover { background: var(--panel-2); }
.setl-t { font-size: 12px; color: var(--fg-3); padding-top: 3px; text-align: right; }
.setl-ic { width: 20px; height: 20px; border-radius: 50%; display: inline-grid; place-items: center; font-size: 11px; line-height: 1;
  background: var(--panel-2); color: var(--fg-3); border: 1px solid var(--line); }
.setl-x { font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; padding-top: 1px; }
.setl-c { font-size: 11px; color: var(--fg-3); margin-right: 6px; }
.k-screen .setl-ic { background: var(--info-bg); color: var(--info); border-color: transparent; }
.k-tap .setl-ic { background: var(--accent-bg); color: var(--accent); border-color: transparent; }
.k-network .setl-ic { background: var(--good-bg); color: var(--good); border-color: transparent; }
.k-log .setl-ic { background: var(--panel-2); color: var(--fg-2); }
.k-app .setl-ic { background: var(--serious-bg); color: var(--serious); border-color: transparent; }
.lv-warn .setl-ic { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
.lv-bad .setl-ic, .k-error .setl-ic { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
.lv-bad .setl-x { color: var(--fg); }
.setl-end { background: var(--bad-bg); margin-top: 4px; }
.setl-end:hover { background: var(--bad-bg); }
.setl-gap { padding: 2px 6px 2px 96px; }
.setl-gap .muted::before, .setl-gap .linkbtn::before { content: '⋮ '; color: var(--fg-3); }
.sestack + .sestack { margin-top: 14px; }
.seframes { margin: 8px 0 0; padding-left: 22px; }
.seframes li { padding: 3px 0; overflow-wrap: anywhere; }
.sefr-lib { list-style: none; margin-left: -22px; }
.sefr-lib summary { cursor: pointer; padding: 2px 0; }
.sefr-lib ol { padding-left: 22px; margin: 4px 0; }
@media (max-width: 900px) {
  .sesplit { grid-template-columns: minmax(0, 1fr); }
  .selist { position: static; max-height: 300px; }
  .setl { max-height: 60vh; }
  .setl-i { grid-template-columns: 46px 22px minmax(0, 1fr); gap: 6px; }
  .setl-gap { padding-left: 80px; }
}

/*
 * Logs → Patterns & storage (logPatternsView in app.js), as three steps:
 * choose what is stored, decide pattern by pattern, see what it saved.
 */
.lpstep-h { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
.lpstep-h h3 { margin: 2px 0 4px; }
.lpstep-h p { margin: 0; max-width: 90ch; }
.lpnum-c { flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent-bg); color: var(--accent); font-weight: 700; font-size: 14px; }
.lpstep3 { margin-top: 14px; }
.lpstep3 .lpstep-h { margin-bottom: 0; }
.lpstore-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 300px); gap: 20px; align-items: start; }
.lpmode { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.lpopt { display: flex; flex-direction: column; gap: 4px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 9px;
  cursor: pointer; margin: 0; font-weight: 400; color: var(--fg); position: relative; }
.lpopt input { position: absolute; opacity: 0; pointer-events: none; }
.lpopt.on { border-color: var(--accent); background: var(--accent-bg); box-shadow: inset 0 0 0 1px var(--accent); }
.lpopt.on > b::before { content: '✓ '; color: var(--accent); }
.lpopt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.lpstats { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; }
.lpstats > div { display: flex; flex-direction: column; }
.lpstats b { font-size: 20px; }
.lpstats b.good { color: var(--good); }
.lpstats span { font-size: 11px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .05em; }
.lpsuggest { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between;
  padding: 12px 14px; margin-bottom: 12px; border-radius: 9px; background: var(--accent-bg); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
.lpsuggest > div { display: flex; flex-direction: column; gap: 2px; }
.lpbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; }
.lpbar select, .lpbar input[type=search] { width: auto; max-width: 240px; padding: 5px 9px; font-size: 13px; }
.lpbar input[type=search] { flex: 1; min-width: 140px; max-width: 280px; }
.lpbulk { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 8px 10px; margin-bottom: 6px;
  background: var(--panel-2); border-radius: 8px; position: sticky; top: 0; z-index: 2; }
.lprow { display: grid; grid-template-columns: 22px minmax(0, 1fr) 230px; gap: 14px; align-items: start;
  padding: 14px 6px; border-bottom: 1px solid var(--line); }
.lprow:last-child { border-bottom: 0; }
.lprow.is-dropped .lptpl { color: var(--fg-3); }
.lprow.v-keep { box-shadow: inset 3px 0 0 var(--good); }
.lprow.v-drop { box-shadow: inset 3px 0 0 var(--fg-3); }
.lpcheck { width: 16px; height: 16px; margin-top: 3px; }
.lptpl { font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.lpvar { display: inline-block; padding: 0 5px; margin: 0 1px; border-radius: 4px; font-size: 11px; line-height: 1.6;
  background: var(--info-bg); color: var(--info); }
.lpmeta { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; margin-top: 6px; }
.lpbarv { width: min(100%, 360px); height: 4px; border-radius: 2px; background: var(--panel-2); overflow: hidden; margin: 7px 0 2px; }
.lpbarv i { display: block; height: 100%; background: var(--s1); border-radius: 2px; }
.lpsample { margin-top: 4px; }
.lpsample summary { cursor: pointer; }
.lpsample pre { margin: 6px 0 0; padding: 8px 10px; background: var(--panel-2); border-radius: 6px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 200px; overflow-y: auto; }
.lpact { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; text-align: right; }
.lpeffect { font-size: 13px; font-weight: 600; }
.lpeffect.stored { color: var(--good); }
.lpeffect.dropped { color: var(--fg-3); }
.lpwhy { max-width: 230px; line-height: 1.35; }
.lpverdict { display: inline-flex; gap: 6px; }
.lpverdict button { font: inherit; font-size: 13px; padding: 6px 14px; border-radius: 7px; cursor: pointer; position: relative;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--fg-2); }
.lpverdict button:hover:not(:disabled) { color: var(--fg); background: var(--panel-2); }
.lpverdict button.proposed { border-color: var(--accent); color: var(--fg); }
.lpverdict button i { position: absolute; top: -8px; right: -6px; font-style: normal; font-size: 9px; text-transform: uppercase; letter-spacing: .04em;
  background: var(--accent); color: var(--accent-ink); padding: 1px 5px; border-radius: 8px; }
.lpverdict button.on.lp-keep { background: var(--good-bg); color: var(--good); border-color: var(--good); font-weight: 600; }
.lpverdict button.on.lp-drop { background: var(--panel-2); color: var(--fg); border-color: var(--fg-3); font-weight: 600; }
.lpverdict button:disabled { cursor: default; opacity: .7; }
.lplock { color: var(--fg-3); }
@media (max-width: 900px) {
  .lpstore-grid { grid-template-columns: minmax(0, 1fr); }
  .lpmode { grid-template-columns: minmax(0, 1fr); }
  .lprow { grid-template-columns: 22px minmax(0, 1fr); }
  .lpact { grid-column: 2; align-items: flex-start; text-align: left; }
  .lpwhy { max-width: none; }
}
/* The filter tabs scroll inside their own width on a phone, rather than pushing the page. */
.lpbar > .pbar { min-width: 0; max-width: 100%; }
.lpverdict { margin-top: 6px; }

/*
 * Settings → Nodes: the live picture of the cluster (app.js clusterFlowHtml).
 * One grid: a node, the gap between, the other node. Row 1 is the edge the
 * reports arrive on, row 2 the names, then one row per part, so a lane in the
 * gap sits level with the two parts it joins whatever the card's width.
 * HTML rather than one scaled SVG: the text stays the page's size.
 */
.cflow { margin: 4px 0 18px; }
.cf-grid {
  display: grid; max-width: 1100px; row-gap: 10px;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr) minmax(0, 1fr);
  /* Named rows, so the shell's `2 / -1` reaches the last one: without them -1 is line 1. */
  grid-template-rows: repeat(6, auto);
}
.cf-shell { background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px; }
.cf-shell.down { border-color: var(--bad); border-style: dashed; }
.cf-shell.removed, .cf-part.removed, .cf-head.removed, .cf-part.down, .cf-head.down { opacity: .55; }
.cf-head, .cf-part, .cf-in, .cf-mid { position: relative; z-index: 1; min-width: 0; }
.cf-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding: 12px 14px 0; }
.cf-head b { font-size: 15px; color: var(--fg); }
.cf-role { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); }
.cf-role.primary { color: var(--info); }
.cf-role.removed { color: var(--bad); }
.cf-part {
  margin: 0 12px; padding: 8px 12px; min-height: 52px; box-sizing: border-box; align-self: start;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
}
.cf-part.last { margin-bottom: 12px; }
.cf-part.on { border-color: var(--accent); }
.cf-part.bad { border-color: var(--bad); }
.cf-k { font-size: 11px; color: var(--fg-3); }
.cf-v { font-size: 14px; font-weight: 600; color: var(--fg); overflow-wrap: anywhere; }

/* The gap: lanes level with the parts' middles, the words under them. */
.cf-mid { grid-column: 2; align-self: start; text-align: center; }
.cf-mid.bottom { align-self: end; padding-bottom: 2px; }
.cf-mid > .cf-lane { margin: 19px -12px 0; }
.cf-band, .cf-pair { margin: 4px -12px 2px; padding: 4px 0; border-radius: 10px; }
.cf-band { border: 1px dashed var(--line); }
.cf-band.live { background: var(--accent-bg); border: 1px solid var(--accent); }
.cf-word { font-size: 12px; line-height: 1.4; color: var(--fg-2); padding: 0 6px; overflow-wrap: anywhere; }
.cf-word.muted { color: var(--fg-3); }
.cf-word.good { color: var(--good); font-weight: 600; }
.cf-word.warn { color: var(--warn); font-weight: 600; }
.cf-word.bad { color: var(--bad); font-weight: 600; }
.cf-sync { font-size: 11.5px; font-weight: 800; letter-spacing: .12em; color: var(--fg-3); }
.cf-sync.live { color: var(--accent); animation: cf-pulse 1.6s ease-in-out infinite; }
@keyframes cf-pulse { 50% { opacity: .55; } }

/* A lane: a track, a head at the receiving end, and dots while something crosses. */
.cf-lane { position: relative; height: 14px; }
.cf-lane::before { content: ''; position: absolute; left: 0; right: 0; top: 6px; height: 2px; background: var(--line-2); }
.cf-lane::after { content: ''; position: absolute; top: 2px; border: 5px solid transparent; }
.cf-lane.ltr::after { right: -2px; border-left: 8px solid var(--line-2); border-right: 0; }
.cf-lane.rtl::after { left: -2px; border-right: 8px solid var(--line-2); border-left: 0; }
.cf-lane.off::before { background: repeating-linear-gradient(90deg, var(--line-2) 0 3px, transparent 3px 8px); }
.cf-lane.off::after { display: none; }
.cf-lane.live::before { background: var(--accent); opacity: .5; }
.cf-lane.live.ltr::after { border-left-color: var(--accent); }
.cf-lane.live.rtl::after { border-right-color: var(--accent); }
.cf-lane.hot::before { background: var(--warn); top: 5px; height: 3px; }
.cf-lane.hot.ltr::after { border-left-color: var(--warn); }
.cf-lane.hot.rtl::after { border-right-color: var(--warn); }
.cf-lane.same::before { background: var(--good); opacity: .7; }
.cf-lane.same.ltr::after { border-left-color: var(--good); }
.cf-lane.same.rtl::after { border-right-color: var(--good); }
.cf-lane.bad::before { background: repeating-linear-gradient(90deg, var(--bad) 0 6px, transparent 6px 10px); }
.cf-lane.bad.ltr::after { border-left-color: var(--bad); }
.cf-lane.bad.rtl::after { border-right-color: var(--bad); }
.cf-dot {
  position: absolute; top: 3px; left: 0; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg);
  animation: cf-ltr 2s linear infinite;
}
.cf-lane.hot .cf-dot { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-bg); }
.cf-lane.rtl .cf-dot { animation-name: cf-rtl; }
@keyframes cf-ltr { from { left: 0; } to { left: calc(100% - 8px); } }
@keyframes cf-rtl { from { left: calc(100% - 8px); } to { left: 0; } }

/* The reports, arriving from above: an edge into the node, not a box. */
.cf-in { display: flex; justify-content: center; align-items: stretch; gap: 10px; height: 58px; margin-bottom: -10px; }
.cf-lane.down { width: 14px; height: auto; }
.cf-lane.down::before { left: 6px; right: auto; top: 0; bottom: 6px; width: 2px; height: auto; }
.cf-lane.down::after { top: auto; bottom: -1px; left: 2px; border: 5px solid transparent; border-top: 8px solid var(--line-2); border-bottom: 0; }
.cf-lane.down.live::after { border-top-color: var(--accent); }
.cf-lane.down.off::before { background: repeating-linear-gradient(180deg, var(--line-2) 0 3px, transparent 3px 8px); }
.cf-lane.down .cf-dot { left: 3px; top: 0; animation-name: cf-down; }
@keyframes cf-down { from { top: 0; } to { top: calc(100% - 10px); } }
.cf-inword { align-self: center; display: flex; flex-direction: column; line-height: 1.2; }
.cf-in.removed, .cf-in.down { opacity: .55; }

@media (prefers-reduced-motion: reduce) {
  .cf-dot { display: none; }
  .cf-sync.live { animation: none; }
}
@media (max-width: 640px) {
  .cf-head { padding: 10px 8px 0; flex-direction: column; align-items: flex-start; gap: 0; }
  .cf-role { font-size: 10px; }
  .cf-sync { font-size: 9.5px; letter-spacing: .04em; }
  .cf-head b { font-size: 13px; }
  .cf-part { margin: 0 6px; padding: 6px 8px; min-height: 46px; }
  .cf-v { font-size: 12px; }
  .cf-word { font-size: 11px; }
  .cf-mid > .cf-lane { margin: 16px -6px 0; }
  .cf-band, .cf-pair { margin: 2px -6px; }
}

/*
 * The applications page: tiles or a table. Two icon buttons joined into one
 * control, the same shape as the range picker beside it.
 */
.layoutpick { display: inline-flex; border: 1px solid var(--line-2); border-radius: 7px; overflow: hidden; margin-right: 10px; }
.layoutpick button {
  background: none; border: 0; border-right: 1px solid var(--line-2); color: var(--fg-2);
  cursor: pointer; padding: 5px 8px; line-height: 0;
}
.layoutpick button:last-child { border-right: 0; }
.layoutpick button:hover { background: var(--panel-2); color: var(--fg); }
.layoutpick button.on { background: var(--accent-bg); color: var(--accent); }
.layoutpick svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.appcell { display: inline-flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; flex-wrap: wrap; }
.appcell-name { font-weight: 600; color: var(--fg); overflow-wrap: anywhere; }
.apptile-kind.sm { width: 26px; height: 26px; border-radius: 7px; }
.apptile-kind.sm svg { width: 15px; height: 15px; }

/* Hosts: reboots per day beside the list of boots; stacked when narrow. */
.bootrow { display: grid; gap: 14px; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); align-items: stretch; }
.bootrow > * { min-width: 0; }
/* The reboots chart and its list of boots in one card (.ly-split inside it). */
.bootrow:has(> .bootcard:only-child) { grid-template-columns: minmax(0, 1fr); }
.bootcard .bootsub { font-size: 12.5px; font-weight: 650; color: var(--fg-2); margin: 0 0 6px; display: flex; gap: 8px; align-items: baseline; }
.bootcard .empty { padding: 8px 0; }
@media (max-width: 1000px) { .bootrow { grid-template-columns: minmax(0, 1fr); } }
.hostcard .hupline.warn { color: var(--warn); }

/* /privacy — the notice on a page of its own (server/subscribe.js). */
.privbody { margin: 0; background: var(--bg); color: var(--fg); }
.privpage { max-width: 720px; margin: 0 auto; padding: 40px 20px 56px; line-height: 1.6; font-size: 15px; }
.privpage h1 { font-size: 26px; margin: 18px 0 8px; letter-spacing: -.01em; }
.privpage h2 { font-size: 15px; margin: 22px 0 4px; color: var(--fg); }
.privpage p { margin: 0 0 8px; color: var(--fg-2); }
.privpage .privlead { color: var(--fg-3); font-size: 14px; }
.privback { display: inline-block; font-weight: 600; text-decoration: none; }
.privback:hover { text-decoration: underline; }
.privfoot { margin-top: 32px !important; padding-top: 16px; border-top: 1px solid var(--line); }
/* /terms: the clauses that limit liability or move it to the customer, set
   apart so they are expressly brought to attention (Ptk. 6:78). */
.privpage .privspecial { border-left: 3px solid var(--warn); padding: 2px 0 2px 14px; margin: 14px 0; }
.privpage .privspecial h2 { margin-top: 6px; }
/* /licenses: a licence text, folded until asked for. */
.privpage details { margin: 0 0 8px; color: var(--fg-2); }
.privpage summary { cursor: pointer; font-size: 13px; }
.privpage .privpre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; line-height: 1.45;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin: 6px 0 0; color: var(--fg-2); }
.privpage ul { color: var(--fg-2); padding-left: 20px; }
/* A wide landing card's second line: the detail under its picture. */
.lf-card .lf-more { margin: 16px 0 0; font-size: 13px; color: var(--fg-2); line-height: 1.5; }

/* A sparkline that can be read: its peak with the unit, and a pointer readout. */
.sparkwrap { position: relative; width: 100%; height: 100%; display: flex; align-items: flex-end; cursor: crosshair; }
.sparkwrap > svg { width: 100%; }
.spark-scale { position: absolute; left: 0; top: -2px; font-size: 11px; color: var(--fg-3); pointer-events: none;
  background: color-mix(in srgb, var(--panel) 80%, transparent); padding: 0 4px 0 0; border-radius: 3px; }
.spark-guide, .spark-dot { position: absolute; display: none; pointer-events: none; }
.spark-guide { top: 0; bottom: 0; width: 1px; background: var(--fg-3); opacity: .6; }
.spark-dot { width: 7px; height: 7px; border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 0 2px var(--panel); }
.sparkwrap.on .spark-guide, .sparkwrap.on .spark-dot { display: block; }
/* Sessions: the replay button at the start of the row. */
.replaybtn { min-width: 34px; justify-content: center; padding-left: 0; padding-right: 0; font-size: 13px; line-height: 1; }
/* A list that could be endless: about ten rows on screen, the rest scrolls, the header stays. */
.cap10 { max-height: 430px; overflow-y: auto; overscroll-behavior: contain; border-radius: 6px; }
.cap10 thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
/* /privacy over the landing page's background video: the text on a panel. */
.privbody .privpage { position: relative; z-index: 1; margin-top: 28px; margin-bottom: 28px; border-radius: 14px;
  background: color-mix(in srgb, var(--panel) 90%, transparent); border: 1px solid var(--line); box-shadow: var(--shadow-lg); }
@media (max-width: 760px) { .privbody .privpage { margin: 0; border-radius: 0; border: 0; } }

/* Hosts: tiles, reboots and boots — stacked, and on a wide screen one row. */
.hosttop { display: grid; gap: 14px; margin-bottom: 14px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1700px) {
  .hosttop { grid-template-columns: minmax(0, .8fr) minmax(0, 2.3fr); align-items: stretch; }
  .hosttop .bootrow { display: contents; }
  .hosttop .hostkpis { grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: stretch; }
  .hosttop .hostkpis .kpi { display: flex; flex-direction: column; justify-content: center; }
}

/* IoT devices list: one row per device, a block per probe with its sparkline. */
/* A device row: the board on the left, read down; its probes beside it on the right. */
.iotrow { display: grid; grid-template-columns: minmax(210px, 270px) minmax(0, 1fr); gap: 12px 20px; align-items: stretch;
  padding: 14px 0; border-top: 1px solid var(--line); min-width: 0; }
.iotrow:first-of-type { border-top: 0; padding-top: 4px; }
.iotrow.stale .iotsens { opacity: .62; }
.iotrow-info { min-width: 0; }
.iotrow-name { font-weight: 650; font-size: 14px; min-width: 0; overflow-wrap: anywhere; }
.iotfacts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 12px; margin: 8px 0 0; min-width: 0; }
.iotfacts dt { color: var(--fg-3); font-size: 11.5px; }
.iotfacts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 640px) { .iotrow { grid-template-columns: minmax(0, 1fr); } }
/* The probes fill their column (asked for): one probe is one tile the full width, several share it evenly and wrap
   at 240 px; a single row of tiles stretches to the height of the facts beside it, and the chart takes that height. */
.iotsensors { display: grid; gap: 10px; min-width: 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.iotsens { display: flex; flex-direction: column; min-width: 0; padding: 9px 11px 8px; color: inherit; text-decoration: none;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; transition: border-color .12s ease; }
.iotsens:hover { border-color: var(--accent); text-decoration: none; }
.iotsens:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.iotsens-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; min-width: 0; font-size: 12px; }
.iotsens-q { font-weight: 600; flex: none; }
.iotsens-p { color: var(--fg-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.iotsens-p.mono { font-size: 11px; }
.iotsens-v { margin-top: 2px; font-size: 13px; }
.iotsens-v b { font-size: 21px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
/* Min, mean and max over the window, under the value: one compact line, wrapping on a narrow tile. */
.iotsens-stats { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 2px; font-size: 12px; font-variant-numeric: tabular-nums; }
.iotsens-stats .k { color: var(--fg-3); font-size: 11px; }
.iotsens-spark { flex: 1 1 auto; display: flex; flex-direction: column; margin-top: 8px; min-height: 36px; }
.iotsens-spark > .muted { margin-top: auto; }
/* The wrap is exactly the plot, so the pointer dot (placed in per cent of it) sits on the line at any height. */
.iotsens-spark .sparkwrap { flex: 1 1 auto; display: block; min-height: 36px; }
/* The chart's own range label is hidden here: the Min / Max above are the real extremes, from every reading,
   and a second, bucket-averaged pair beside them read as a contradiction. The pointer readout stays. */
.iotsens-spark .spark-scale { display: none; }
.iotsens-spark svg { position: absolute; inset: 0; display: block; width: 100%; height: 100% !important; }
/* Asked for: on a wide screen the list is at most half of it, so a device, its facts and its probes read as one block
   instead of a name at one edge and its facts at the other. Full width below that, and two probes a row on a phone. */
@media (min-width: 1400px) { .iotlist { max-width: 50%; } }
@media (max-width: 600px) { .iotsensors { grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr)); } }

/* A figure in a table cell, coloured by what it means (sign-ins, SSH doors). The badge keeps its own. */
.tablewrap td .good:not(.badge) { color: var(--good); }
.tablewrap td .bad:not(.badge) { color: var(--bad); }
.tablewrap td .warn:not(.badge) { color: var(--warn); }

/* A card whose table needs the whole pane (the sign-in book): across both columns of the flow. */
@media (min-width: 1240px) { .flow > .span { column-span: all; } }
/* Several badges in one cell (the sign-in book's "stands out"): they wrap, bounded, instead of widening the row. */
.flagwrap { display: inline-flex; flex-wrap: wrap; gap: 4px; max-width: 260px; white-space: normal; vertical-align: middle; }

/* The organisation switch in the user menu (orgs.js): the current one marked, the rest one click. */
.usermenu-orgs { padding: 6px 12px 8px; border-bottom: 1px solid var(--line); }
.usermenu-orgs .k { font-size: 10.5px; letter-spacing: .07em; text-transform: uppercase; color: var(--fg-3); font-weight: 650; margin-bottom: 4px; }
.usermenu-orgs .e { font-size: 12.5px; color: var(--fg-2); }
.usermenu-orgs button { display: block; width: calc(100% + 16px); text-align: left; padding: 5px 8px; margin: 0 -8px; border: 0; background: none; color: var(--fg); font: inherit; border-radius: 6px; cursor: pointer; }
.usermenu-orgs button:hover { background: var(--panel-2); }
.usermenu-orgs button.on { color: var(--accent); font-weight: 650; }
.usermenu-orgs button.on::before { content: '● '; }

/* "Confirm your address" (register.js): one line under the top bar, gone the moment it is done. */

/*
 * The front door, said loudly (register.js). A button under the headline —
 * where the eye lands first — with a live dot that says the install takes
 * new people now, and a card of its own under the sign-in for somebody who
 * came looking for a way in and has no account.
 */
.landing-cta { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; margin: 6px 0 22px; }
.cta-live { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 650; color: var(--good); }
.cta-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--good); box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 60%, transparent); animation: cta-pulse 2s infinite; }
@keyframes cta-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 55%, transparent); } 70% { box-shadow: 0 0 0 10px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
@media (prefers-reduced-motion: reduce) { .cta-dot { animation: none; } }
.cta-big { font-size: 17px; padding: 14px 24px; border-radius: 12px; box-shadow: 0 10px 28px color-mix(in srgb, var(--accent) 35%, transparent); transition: transform .15s ease, box-shadow .15s ease; }
.cta-big:hover { transform: translateY(-1px); box-shadow: 0 14px 34px color-mix(in srgb, var(--accent) 45%, transparent); }
.cta-points { list-style: none; padding: 0; margin: 4px 0 0; display: grid; gap: 8px; color: var(--fg-2); font-size: 15px; }
.cta-points li::before { content: '✓'; color: var(--good); font-weight: 700; margin-right: 10px; }

/* Continue with Google (oauth.js): full width, the provider's mark, no brand colours of ours. */
.oauth-box { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.oauth-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; text-decoration: none; }
.oauth-mark { display: inline-flex; width: 22px; height: 22px; align-items: center; justify-content: center; color: var(--fg-2); }
/* Terms and privacy, small, at the foot of the sign-in and registration cards (legal.js). */
.legal-note { margin: 14px 0 0; font-size: 12px; line-height: 1.5; color: var(--fg-3); text-align: center; }
.legal-note a { color: var(--fg-2); }
/* The ways in, one under the other, no "or" between them. */
.landing-form .oauth-box { margin-top: 12px; }
.landing-form .qrsign { margin-top: 8px; }
/*
 * The way in and the way to read on, side by side and the same height
 * (loginView). "What it does" keeps its underline everywhere else; in the
 * row it is a quiet glass button beside the loud one.
 */
.landing-actions { display: flex; flex-wrap: wrap; align-items: stretch; gap: 12px; margin: 26px 0 14px; }
.landing-actions .cta-big { display: inline-flex; align-items: center; gap: 10px; font-size: 17px; font-weight: 650; padding: 14px 26px; }
.landing-actions .cta-big .cta-arrow { transition: transform .15s ease; }
.landing-actions .cta-big:hover .cta-arrow { transform: translateX(3px); }
.landing-actions .landing-more {
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent); border-radius: 12px; padding: 14px 22px;
  background: color-mix(in srgb, var(--panel) 55%, transparent); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  font-size: 16px; transition: border-color .15s ease, background .15s ease;
}
.landing-actions .landing-more:hover { border-color: color-mix(in srgb, var(--fg) 45%, transparent); background: color-mix(in srgb, var(--panel) 75%, transparent); }
.landing-pitch .cta-live { font-size: 13px; font-weight: 600; letter-spacing: .01em; }

/*
 * The front page's boxes over the video (asked for on 2026-10-26): the
 * sign-in and registration cards, the feature cards, the sign-up card and
 * the terms and privacy pages are see-through to 1%, so the video runs
 * behind them too. Their contrast is still the veil's — the page colour at
 * 86% over the video — which is what the text was measured against.
 * The fields keep a little body so they still read as fields.
 */
.landing .landing-form,
.landing .lf-card,
.landing .lsub-card,
.privbody .privpage {
  background: color-mix(in srgb, var(--panel) 1%, transparent);
  border-color: color-mix(in srgb, var(--fg) 14%, transparent);
  box-shadow: none;
}
.landing .lf-card::before { opacity: .55; }
.landing .landing-form input,
.landing .lsub-card input {
  background: color-mix(in srgb, var(--bg) 35%, transparent);
  border-color: color-mix(in srgb, var(--fg) 22%, transparent);
}
.landing .landing-form .oauth-btn,
.landing .landing-form .qrsign .btn:not(.primary) {
  background: color-mix(in srgb, var(--panel) 1%, transparent);
  border-color: color-mix(in srgb, var(--fg) 22%, transparent);
}

/*
 * The Host tab (installView): Linux and Windows side by side, each its
 * command and its Copy; one under the other on a phone. The box the
 * address named (#/install/windows) wears the accent.
 */
.osgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-bottom: 14px; }
@media (max-width: 900px) { .osgrid { grid-template-columns: minmax(0, 1fr); } }
.osbox { display: flex; flex-direction: column; }
.osbox.on { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.osbox-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.osbox-ico { width: 40px; height: 40px; flex: 0 0 auto; border-radius: 10px; display: grid; place-items: center; background: var(--accent-bg); color: var(--accent); }
.osbox .snippet { flex: 1 1 auto; }
.osbox-copy { min-width: 96px; }
.osdetails > summary { cursor: pointer; display: flex; gap: 10px; align-items: baseline; }
.osdet-h { margin: 18px 0 8px; font-size: 13px; letter-spacing: .02em; }
/* "Start here" on an install tab with nothing to install yet: the one thing to do, loud. */
.startcall { display: flex; flex-wrap: wrap; gap: 14px 24px; align-items: center; justify-content: space-between;
  padding: 18px 20px; border-radius: 12px; background: var(--accent-bg); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
.startcall > div { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 260px; }
.startcall b { font-size: 16px; color: var(--fg); }

/* Home (homeView): where things stand, or — with nothing connected — what to do first. */
.home { display: flex; flex-direction: column; gap: 14px; }
.home-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
.home-hello { margin: 0 0 2px; font-size: 24px; letter-spacing: -.01em; }
.home-start { padding: 26px 26px 22px; }
.home-start-h { font-size: 19px; margin: 0 0 6px; }
.home-paths { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 980px) { .home-paths { grid-template-columns: minmax(0, 1fr); } }
.home-path { position: relative; display: flex; flex-direction: column; gap: 8px; padding: 20px; border-radius: 14px; border: 1px solid var(--line);
  background: var(--panel-2); color: var(--fg); text-decoration: none; transition: border-color .15s ease, transform .15s ease; }
.home-path:hover { border-color: var(--accent); transform: translateY(-2px); }
.home-path.rec { border-color: color-mix(in srgb, var(--accent) 60%, transparent); background: var(--accent-bg); }
.home-path b { font-size: 16px; }
.home-path > span:not(.btn):not(.home-badge):not(.home-path-ico) { color: var(--fg-2); font-size: 14px; line-height: 1.45; flex: 1 1 auto; }
.home-path .btn { align-self: flex-start; margin-top: 4px; }
.home-path-ico { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; background: var(--panel); color: var(--accent); }
.home-badge { position: absolute; top: 14px; right: 14px; font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px; background: var(--accent); color: var(--accent-ink); }
.home-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 980px) { .home-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.home-tile { display: flex; flex-direction: column; gap: 4px; color: var(--fg); text-decoration: none; }
a.home-tile:hover { border-color: var(--accent); }
.home-tile .k { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-3); }
.home-tile b { font-size: 28px; font-weight: 800; letter-spacing: -.02em; }
.home-tile b.good { color: var(--good); } .home-tile b.warn { color: var(--warn); } .home-tile b.bad { color: var(--bad); }
.home-tile .bad { color: var(--bad); }
.home-lists { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); gap: 14px; }
.home-row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 2px; border-top: 1px solid var(--line); color: var(--fg); text-decoration: none; }
.home-row:hover { color: var(--accent); }
.home-row .bad { color: var(--bad); }
.home-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; vertical-align: middle; }
.home-dot.on { background: var(--good); } .home-dot.off { background: var(--bad); }
.home-bar { height: 6px; border-radius: 999px; background: var(--panel-2); overflow: hidden; margin: 0 0 12px; }
.home-bar span { display: block; height: 100%; background: var(--accent); border-radius: inherit; }
.home-checklist { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
.home-checklist li { display: flex; align-items: center; gap: 10px; }
.home-checklist li.done > span:nth-child(2) { color: var(--fg-3); text-decoration: line-through; }
.home-check { width: 22px; height: 22px; flex: 0 0 auto; border-radius: 50%; border: 2px solid var(--line-2); display: grid; place-items: center; font-size: 12px; font-weight: 800; }
.home-checklist li.done .home-check { background: var(--good); border-color: var(--good); color: var(--bg); }
/* Device keys (iotKeysCard): the switch reads as a state, coloured by what it does. */
.iotkey-switch { display: inline-flex; align-items: center; gap: 8px; margin: 0; cursor: pointer; }
.iotkey-switch .good { color: var(--good); font-weight: 600; font-size: 13px; }
.iotkey-switch .bad { color: var(--bad); font-weight: 600; font-size: 13px; }

/* Confirm your email (confirmView): the only screen an unconfirmed address gets. */
.sidebar.locked { opacity: .45; filter: grayscale(.6); pointer-events: none; user-select: none; }
.confirm-main { flex: 1 1 auto; min-width: 0; }
/* The locked menu says whose product this is above 800px; below it the menu is a closed drawer, and the card says it. */
@media (min-width: 801px) { .confirm-main .confirm-card > .brand { display: none; } }
.confirm-page { min-height: 100vh; min-height: 100dvh; display: grid; place-items: center; padding: 24px 16px; background: var(--bg);
  background-image: radial-gradient(ellipse 60% 45% at 50% 30%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 70%); }
.confirm-card { width: min(100%, 440px); display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 28px 28px 22px; text-align: center; }
.confirm-card .brand { align-self: flex-start; font-size: 14px; }
.confirm-ico { position: relative; width: 76px; height: 76px; margin-top: 6px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent-bg); color: var(--accent); }
.confirm-ping { position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--accent); opacity: 0; animation: confirm-ping 2.4s ease-out infinite; }
@keyframes confirm-ping { 0% { transform: scale(.9); opacity: .55; } 100% { transform: scale(1.45); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .confirm-ping { animation: none; } }
.confirm-card h2 { margin: 2px 0 0; font-size: 22px; letter-spacing: -.01em; }
.confirm-card p { margin: 0; color: var(--fg-2); line-height: 1.5; overflow-wrap: anywhere; }
.confirm-card p b { color: var(--fg); }
.confirm-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; width: 100%; margin-top: 4px; }
.confirm-actions .btn { flex: 1 1 160px; justify-content: center; padding: 11px 16px; font-size: 15px; }
.confirm-wait { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg-3); }
.confirm-foot { width: 100%; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; color: var(--fg-3); }

/* Home's living map (homeNetCard): Obscenter at the top, the web under it, the outside at the sides. */
.home-net-legend span { display: inline-flex; align-items: center; gap: 6px; }
.net-key { width: 10px; height: 10px; display: inline-block; border-radius: 50%; }
.net-key-app { background: var(--c-app); }
.net-key-service { background: var(--c-service); border-radius: 2px; transform: rotate(45deg) scale(.85); }
.net-key-data { background: var(--c-service); border-radius: 4px; height: 8px; }
.net-key-host { background: var(--c-host); border-radius: 2px; width: 12px; height: 8px; }
.net-key-out { border: 2px solid var(--c-domain); background: transparent; }
.net { display: block; width: 100%; height: auto; font-family: inherit; }
.net-grid { fill: var(--line); }
.net-side { fill: var(--fg-3); font-size: 11px; letter-spacing: .12em; font-weight: 700; }
.net-up { fill: none; stroke: var(--line-2); stroke-width: 1; stroke-dasharray: 2 5; opacity: .7; }
.net-dot { opacity: .9; }
.net-edge { fill: none; stroke: var(--fg-3); opacity: .55; stroke-dasharray: 7 5; animation: net-flow 1.6s linear infinite; }
.net-edge.ext { stroke: var(--c-domain); opacity: .45; stroke-dasharray: 3 5; animation-duration: 2.4s; }
.net-edge.warn { stroke: var(--warn); opacity: .85; }
.net-edge.bad { stroke: var(--bad); opacity: .9; animation-duration: .9s; }
@keyframes net-flow { to { stroke-dashoffset: -24; } }
.net-node { cursor: pointer; }
.net-shape { --k: var(--accent); }
.net-shape > * { fill: color-mix(in srgb, var(--k) 22%, var(--panel)); stroke: var(--k); stroke-width: 2; transition: transform .15s ease; }
.net-out .net-shape > * { fill: var(--panel); }
.net-node text { fill: var(--fg-2); font-size: 11.5px; paint-order: stroke; stroke: var(--panel); stroke-width: 3px; stroke-linejoin: round; }
.net-alarm { fill: none; stroke-width: 2; animation: net-alarm 1.8s ease-out infinite; }
.h-warn .net-alarm { stroke: var(--warn); }
.h-bad .net-alarm { stroke: var(--bad); }
@keyframes net-alarm { 0% { opacity: .9; transform: scale(.85); } 100% { opacity: 0; transform: scale(1.5); } }
.net-hub rect { fill: var(--panel); stroke: var(--accent); stroke-width: 2; filter: drop-shadow(0 6px 18px color-mix(in srgb, var(--accent) 35%, transparent)); }
.net-hub-mark { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.net-hub-name { fill: var(--fg); font-size: 20px; font-weight: 800; letter-spacing: -.01em; }
.net-pulse { fill: none; stroke: var(--accent); stroke-width: 2; transform-box: fill-box; transform-origin: center; animation: net-pulse 2.8s ease-out infinite; }
.net-pulse.two { animation-delay: 1.4s; }
@keyframes net-pulse { 0% { opacity: .6; transform: scale(1); } 100% { opacity: 0; transform: scale(2.4); } }
.net-alarm { transform-box: fill-box; transform-origin: center; }
.net-big { fill: var(--fg); font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.net-src-b rect { fill: var(--panel); stroke: var(--line-2); }
.net-src-path { stroke: var(--accent); opacity: .5; }
.net-src-b text { fill: var(--fg); font-size: 14px; font-weight: 800; }
.net-src-i circle, .net-src-i path { fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.net-unit { fill: var(--fg-3); font-size: 13px; font-weight: 600; letter-spacing: 0; }
.net-kinds { fill: var(--fg-3); font-size: 12px; }
@media (prefers-reduced-motion: reduce) {
  .net-edge, .net-pulse, .net-alarm { animation: none; }
  .net-dot { display: none; }
}

/* Home's tiles: five, an icon each, the colour of what they say. */
.home-tiles { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 1280px) { .home-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 720px) { .home-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.home-tile { position: relative; padding-right: 54px; }
.home-ico { position: absolute; top: 14px; right: 14px; width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
  color: var(--tone, var(--accent)); background: color-mix(in srgb, var(--tone, var(--accent)) 14%, transparent); }
.home-tile.t-good { --tone: var(--good); }
.home-tile.t-warn { --tone: var(--warn); }
.home-tile.t-bad { --tone: var(--bad); }
.home-tile.t-app { --tone: var(--c-app); }
.home-tile.t-host { --tone: var(--c-host); }
.home-tile.t-muted { --tone: var(--fg-3); }
.home-tile b { color: var(--tone, var(--fg)); }
.home-tile.t-app b, .home-tile.t-host b, .home-tile.t-good b { color: var(--fg); }
@media (max-width: 720px) { .home-tiles > :last-child:nth-child(odd) { grid-column: 1 / -1; } }

/* The map, second pass: the legend over the picture, the outside in its own panels, the machines in a rack, focus and the picked node's card. */
.home-net { position: relative; padding: 8px; overflow: hidden; }
.home-net-legend { position: absolute; z-index: 2; top: 16px; left: 50%; transform: translateX(-50%); display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 14px;
  font-size: 11.5px; color: var(--fg-3); pointer-events: none; }
@media (max-width: 720px) { .home-net-legend { position: static; transform: none; padding: 4px 6px 8px; } }
.home-net-legend span { display: inline-flex; align-items: center; gap: 6px; }
.home-net-svg { position: relative; width: 100%; }
.net-panel { fill: color-mix(in srgb, var(--c-domain) 4%, transparent); stroke: color-mix(in srgb, var(--c-domain) 30%, transparent); stroke-dasharray: 4 4; }
.net-park-bg { fill: color-mix(in srgb, var(--c-host) 6%, var(--panel-2)); stroke: color-mix(in srgb, var(--c-host) 45%, var(--line)); stroke-width: 1.5; }
.net-park-rail { fill: color-mix(in srgb, var(--c-host) 16%, var(--panel-2)); }
.net-rack-stripe { fill: color-mix(in srgb, var(--c-host) 40%, transparent); }
.net-park .net-side { fill: color-mix(in srgb, var(--c-host) 70%, var(--fg)); }
.net-slot { stroke: var(--k); stroke-width: 1.6; stroke-linecap: round; fill: none; }
.net-led { fill: var(--good); }
.h-bad .net-led { fill: var(--bad); }
.net-shape .net-slot { fill: none; }
.net-hit { fill: transparent; }
.net-node:hover .net-shape > :first-child, .net-node:focus .net-shape > :first-child, .net-node.on .net-shape > :first-child { fill: color-mix(in srgb, var(--k) 45%, var(--panel)); }
.net-node:hover text, .net-node.on text { fill: var(--fg); font-weight: 700; }
.net-node:focus { outline: none; }
.net-node:focus-visible .net-hit { stroke: var(--accent); stroke-width: 2; }
.net .net-node, .net .net-edge, .net .net-upg { transition: opacity .18s ease; }
.net.focus .net-node:not(.on) { opacity: .16; }
.net.focus .net-edge:not(.on) { opacity: .05; }
.net.focus .net-upg:not(.on) { opacity: .08; }
.net.focus .net-edge.on { opacity: 1; stroke-width: 3px; }
.net-tip { position: absolute; z-index: 3; box-shadow: var(--shadow-lg); white-space: nowrap; }

/* A new account's Home (setupJourney): one compact column; a done step is a struck line, the current one the only card. */
.journey { container: journey / inline-size; max-width: 880px; margin: 8px auto 0; padding: 16px 18px 14px; border-radius: 14px; background: var(--panel);
  border: 1px solid var(--line); box-shadow: var(--shadow); }
.journey-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; margin-bottom: 12px; }
.journey-head h2 { margin: 0; font-size: 18px; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jring { position: relative; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
  background: conic-gradient(var(--accent) calc(var(--p, 0) * 1%), var(--line-2) 0); }
.jring::before { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--panel); }
.jring b { position: relative; font-size: 11.5px; font-weight: 800; letter-spacing: -.02em; }
.jleft { font-size: 12px; font-weight: 600; color: var(--fg-3); white-space: nowrap; }
.home-bar > i { display: block; height: 100%; background: var(--accent); border-radius: inherit; transition: width .4s ease; }
.jsteps { list-style: none; margin: 0; padding: 0; position: relative; display: grid; gap: 2px; }
.jsteps::before { content: ""; position: absolute; left: 23px; top: 14px; bottom: 14px; width: 2px; background: var(--line); }
.jstep:last-child::after { content: ""; position: absolute; left: 22px; width: 4px; top: calc(50% + 13px); bottom: -1px; background: var(--panel); }
.jstep { position: relative; display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; column-gap: 12px; padding: 7px 10px; border-radius: 10px;
  border: 1px solid transparent; }
.jdot { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; justify-self: center; font-weight: 800; font-size: 12px; position: relative; z-index: 1;
  background: var(--panel); border: 2px solid var(--line-2); color: var(--fg-3); }
.jtitle { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 10px; min-width: 0; font-size: 14px; }
.jtitle b { font-weight: 650; }
.jsub { font-size: 12.5px; color: var(--fg-3); }
.jact { display: inline-flex; align-items: center; justify-self: end; }
.jact:empty { display: none; }
.jstep.done { padding-block: 2px; }
.jstep.done .jdot { width: 18px; height: 18px; font-size: 10px; border: 0; background: var(--good); color: var(--bg); }
.jstep.done .jtitle { font-size: 12.5px; }
.jstep.done .jtitle b { color: var(--fg-3); font-weight: 500; text-decoration: line-through; }
.jstep.next .jtitle b { color: var(--fg-2); }
.jstep.now { margin: 4px 0; padding: 10px; border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
.jstep.now .jtitle { font-size: 15px; }
.jstep.now .jdot { width: 28px; height: 28px; background: var(--accent); border-color: var(--accent); color: var(--accent-ink); animation: jpulse 2.2s ease-out infinite; }
@keyframes jpulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); } 100% { box-shadow: 0 0 0 9px transparent; } }
.jbody { grid-column: 2 / -1; margin-top: 10px; }
.jpaths { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.jpath { position: relative; display: flex; gap: 10px; align-items: center; padding: 9px 10px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel);
  color: var(--fg); text-decoration: none; transition: border-color .15s ease, transform .15s ease; }
.jpath:hover { border-color: var(--accent); transform: translateY(-1px); }
.jpath.rec { border-color: var(--accent); background: var(--accent-bg); }
.jpath > span:nth-child(2) { display: grid; gap: 1px; min-width: 0; font-size: 13px; }
.jpath > span:nth-child(2) > span { color: var(--fg-3); font-size: 11.5px; }
.jpath-ico { flex: 0 0 auto; width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; background: var(--panel-2); color: var(--accent); }
.jpath-ico svg { width: 18px; height: 18px; }
.jbadge { position: absolute; top: -8px; right: 8px; font-size: 9px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; padding: 1px 6px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink); }
.jmore { margin-top: 8px; font-size: 12px; color: var(--fg-3); }
.jwait { display: inline-flex; align-items: baseline; gap: 5px; font-size: 12.5px; color: var(--fg-3); }
.jwait .cta-dot { align-self: center; }
.jwait b { color: var(--fg); font-size: 14px; font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .jstep.now .jdot { animation: none; } .jpath, .jpath:hover { transition: none; transform: none; } }
@container journey (max-width: 600px) { .jpaths { grid-template-columns: minmax(0, 1fr); } }
@container journey (max-width: 420px) {
  .jstep:not(.done) { align-items: start; }
  .jstep:not(.done):last-child::after { top: 34px; }
  .jact { grid-column: 2 / -1; justify-self: start; margin-top: 6px; }
}

/* The legend at the left; a new account's steps inside the map, the picture behind them. */
.home-net-legend { left: 22px; transform: none; justify-content: flex-start; }
.home-net-journey .home-net-svg { min-height: 420px; display: flow-root; }
.home-net-journey .home-net-svg > svg.net { position: absolute; inset: 0; }
/* Under the hub, centred between the two Internet panels, never wider than a column one reads. */
.home-net-journey .journey { position: relative; z-index: 2; max-width: min(860px, calc(100% - 2 * var(--side, 0px) - 56px)); margin: 180px auto 36px; }
@media (max-width: 720px) { .home-net-journey .journey { max-width: none; margin: 160px 4px 8px; } }
svg.net { animation: net-in .35s ease; }
@keyframes net-in { from { opacity: .35; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { svg.net { animation: none; } }

/* Home's figures on the map (homeStats): see-through bubbles, in the top corners when there is room, a row above the map when not. */
.net-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 34px 6px 8px; }
.net-stats .ns-side { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
@media (max-width: 720px) { .net-stats { grid-template-columns: minmax(0, 1fr); padding-top: 4px; } }
.home-net.stats-over .net-stats { position: absolute; z-index: 2; top: 40px; left: 22px; right: 22px; padding: 0; display: flex; justify-content: space-between; pointer-events: none; }
.home-net.stats-over .ns-side { display: flex; gap: 10px; }
.home-net.stats-over .nstat { width: 214px; pointer-events: auto; }
.nstat { --tone: var(--accent); position: relative; display: grid; grid-template-columns: auto 1fr; grid-template-areas: "ico k" "ico v" "sub sub"; column-gap: 10px; row-gap: 1px;
  padding: 10px 12px; border-radius: 14px; text-decoration: none; color: var(--fg);
  background: color-mix(in srgb, var(--panel) 62%, transparent); border: 1px solid color-mix(in srgb, var(--tone) 38%, var(--line));
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); box-shadow: 0 6px 22px color-mix(in srgb, var(--tone) 14%, transparent);
  transition: transform .15s ease, border-color .15s ease; }
.nstat:hover { transform: translateY(-2px); border-color: var(--tone); }
.nstat.t-good { --tone: var(--good); }
.nstat.t-warn { --tone: var(--warn); }
.nstat.t-bad { --tone: var(--bad); }
.nstat.t-app { --tone: var(--c-app); }
.nstat.t-host { --tone: var(--c-host); }
.nstat.t-muted { --tone: var(--fg-3); }
.nstat-ico { grid-area: ico; width: 34px; height: 34px; align-self: center; border-radius: 10px; display: grid; place-items: center;
  color: var(--tone); background: color-mix(in srgb, var(--tone) 16%, transparent); }
.nstat-ico svg { width: 19px; height: 19px; }
.nstat-k { grid-area: k; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); }
.nstat b { grid-area: v; font-size: 22px; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; color: var(--fg); }
.nstat.t-bad b, .nstat.t-warn b { color: var(--tone); }
.nstat b small { font-size: 12px; color: var(--fg-3); font-weight: 700; }
.nstat-sub { grid-area: sub; font-size: 11.5px; color: var(--fg-3); margin-top: 4px; line-height: 1.35; }
.nstat-sub .bad { color: var(--bad); }

/* The brand is the way home (sidebar): the name opens Home, and a small house says so. */
.brandhome { display: flex; align-items: center; gap: 9px; flex: 1 1 auto; min-width: 0; color: inherit; text-decoration: none; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; transition: background .15s ease; }
.brandhome:hover, .brandhome:focus-visible { background: var(--panel-2); }
/* A summary node's count (netSummary), written inside it. */
.net-node text.net-count { fill: var(--fg); font-size: 16px; font-weight: 800; stroke-width: 0; }
.net-out text.net-count { font-size: 15px; }

/* Home, the first row under the name (sidebar). */
a.navtool { text-decoration: none; }
.g-home { --g: var(--accent); }
.navtool.navhome-top.active { background: var(--accent-bg); color: var(--fg); font-weight: 650; }
.navtool.navhome-top.active .appicon { background: var(--accent); color: var(--accent-ink); }
.net-month { fill: var(--fg-3); font-size: 12px; font-weight: 600; }

/* Problems with nothing open: green, still, and as easy to find (problemNavLink). */
.navtopo.problemlink.calm { border-color: color-mix(in srgb, var(--good) 40%, var(--line)); background: color-mix(in srgb, var(--good) 8%, transparent); color: var(--good); }
.navtopo.problemlink.calm .spark { background: var(--good); }
.navtopo.problemlink.calm:hover { background: color-mix(in srgb, var(--good) 14%, transparent); color: var(--good); }

/* The INTERNET frame: see-through, but a frame you can see (asked for on 2026-10-26). */
.net-panel { fill: color-mix(in srgb, var(--c-domain) 5%, transparent); stroke: color-mix(in srgb, var(--c-domain) 55%, var(--line)); stroke-width: 1.3; stroke-dasharray: 6 4; }
/* An outside place's own icon, in its circle (favicon.js): it never takes the pointer from the node. */
.net-fav { pointer-events: none; }
/* An outside place's own icon, in its circle (favicon.js): it never takes the pointer from the node. */
.net-fav { pointer-events: none; }
/* Nothing on Home underlines on hover (asked for on 2026-10-26): the bubbles, the steps and the buttons say they are links by their shape. */
.home-net a, .home-net a:hover, .home-net a:focus, .home-net a *, .home-net a:hover * { text-decoration: none; }

/* An outside place with its own icon drawn (netFavicons); without one the circle stays empty. */
.net-out.has-fav .net-shape > * { fill: var(--panel); }

/* ------------------------------------------------------- the public site --- */
/*
 * /docs, /blog, /quickstart (server/site.js): server-rendered pages around
 * the dashboard, always dark like the front page. No video behind them —
 * these are pages people read and search engines time, and the largest
 * paint should be the words.
 */
body.site { height: auto; font-size: 16px; line-height: 1.65;
  background: radial-gradient(1200px 520px at 15% -10%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%),
    radial-gradient(900px 420px at 95% 0%, color-mix(in srgb, var(--ai) 12%, transparent), transparent 70%), var(--bg); }
.site-wrap { width: 100%; max-width: 1240px; margin: 0 auto; padding: 0 24px; }
.site-skip { position: absolute; left: -9999px; }
.site-skip:focus { left: 12px; top: 12px; z-index: 10; background: var(--panel); padding: 8px 12px; border-radius: 8px; }
.site-head { position: sticky; top: 0; z-index: 5; backdrop-filter: blur(10px);
  background: color-mix(in srgb, var(--bg) 78%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); }
.site-headin { display: flex; align-items: center; gap: 28px; min-height: 64px; }
.site-brand { display: inline-flex; align-items: center; gap: 10px; color: var(--fg); font-weight: 700; font-size: 17px; letter-spacing: -.01em; }
.site-brand:hover { text-decoration: none; }
.site-logo svg { width: 28px; height: 28px; display: block; }
.site-logo rect { fill: var(--accent); }
.site-logo path { stroke: var(--accent-ink); stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.site-nav { display: flex; gap: 22px; flex: 1; }
.site-nav a { color: var(--fg-2); font-weight: 500; font-size: 15px; }
.site-nav a:hover, .site-nav a[aria-current] { color: var(--fg); text-decoration: none; }
.site-nav a[aria-current] { box-shadow: inset 0 -2px 0 var(--accent); padding-bottom: 4px; }
.site-cta { display: flex; align-items: center; gap: 16px; }
.site-signin { color: var(--fg-2); font-weight: 500; font-size: 15px; }
.site .btn { font-size: 15px; padding: 9px 16px; border-radius: 9px; }
.site-kicker { color: var(--accent); font-weight: 600; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; margin: 48px 0 10px; }
.site h1 { font-size: clamp(30px, 4.2vw, 46px); line-height: 1.12; letter-spacing: -.025em; margin: 0 0 14px; color: var(--fg); }
.site-lead { font-size: 19px; color: var(--fg-2); max-width: 760px; margin: 0 0 28px; }
.site-crumbs { margin: 40px 0 10px; color: var(--fg-3); font-size: 14px; }
.site-crumbs a { color: var(--fg-3); }
main#main { min-height: 60vh; padding-bottom: 72px; }

/* Prose: the bodies written in site-docs.js and site-blog.js. */
.site-prose { max-width: 760px; color: var(--fg-2); }
.site-prose h2 { color: var(--fg); font-size: 26px; letter-spacing: -.015em; line-height: 1.25; margin: 44px 0 12px; scroll-margin-top: 84px; }
.site-prose h3 { color: var(--fg); font-size: 19px; margin: 30px 0 8px; scroll-margin-top: 84px; }
.site-prose p, .site-prose ul, .site-prose ol { margin: 0 0 16px; }
.site-prose li { margin: 4px 0; }
.site-prose strong, .site-prose b { color: var(--fg); }
.site-prose a { text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent); text-underline-offset: 3px; }
.site-prose code { font-family: var(--mono); font-size: .86em; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; color: var(--fg); }
.site-prose pre { background: var(--void); color: var(--void-fg); border: 1px solid var(--void-line); border-radius: 12px; padding: 16px 18px; overflow-x: auto; font-size: 14px; line-height: 1.55; margin: 0 0 20px; }
.site-prose pre code { background: none; border: 0; padding: 0; color: inherit; font-size: inherit; }
.site-prose table { width: 100%; border-collapse: collapse; margin: 0 0 20px; font-size: 15px; display: block; overflow-x: auto; }
.site-prose th, .site-prose td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
.site-prose th { color: var(--fg); font-weight: 600; background: color-mix(in srgb, var(--panel) 60%, transparent); }
.site-prose blockquote { margin: 0 0 18px; padding: 4px 18px; border-left: 3px solid var(--accent); color: var(--fg); }
.site-prose .callout { background: color-mix(in srgb, var(--accent) 10%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 12px; padding: 14px 18px; margin: 0 0 20px; color: var(--fg); }
.site-prose .callout p:last-child { margin-bottom: 0; }
.site-fig { margin: 26px 0 30px; }
.site-fig img { width: 100%; height: auto; display: block; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  box-shadow: 0 24px 60px -24px color-mix(in srgb, var(--void) 80%, transparent); background: var(--panel); }
.site-fig figcaption { color: var(--fg-3); font-size: 14px; margin-top: 10px; text-align: center; }

/* Docs: the list of pages, the page, and what is on it. */
.docs { display: grid; grid-template-columns: 250px minmax(0, 1fr) 210px; gap: 44px; align-items: start; }
.docs-side { position: sticky; top: 84px; max-height: calc(100vh - 100px); overflow-y: auto; padding: 36px 6px 24px 0; font-size: 14.5px; }
.docs-side h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); margin: 18px 0 6px; }
.docs-side ul { list-style: none; margin: 0; padding: 0; }
.docs-side li a { display: block; color: var(--fg-2); padding: 4px 10px; border-radius: 7px; line-height: 1.35; }
.docs-side li a:hover { background: color-mix(in srgb, var(--fg) 7%, transparent); color: var(--fg); text-decoration: none; }
.docs-side li a[aria-current] { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--fg); font-weight: 600; }
.docs-find { width: 100%; font: inherit; font-size: 14px; padding: 8px 12px; border-radius: 9px; border: 1px solid var(--line); background: var(--panel); color: var(--fg); }
.docs-none { color: var(--fg-3); font-size: 14px; }
.docs-main { min-width: 0; }
.docs-main > .site-prose { max-width: 780px; }
.docs-toc { position: sticky; top: 84px; padding-top: 44px; font-size: 13.5px; }
.docs-toc h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); margin: 0 0 8px; }
.docs-toc ul { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--line); }
.docs-toc a { display: block; padding: 3px 0 3px 12px; color: var(--fg-3); line-height: 1.35; }
.docs-toc a:hover { color: var(--fg); text-decoration: none; }
.docs-groups { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 18px; margin-top: 10px; }
.docs-card { background: color-mix(in srgb, var(--panel) 55%, transparent); border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); border-radius: 14px; padding: 18px 20px; }
.docs-card h2 { font-size: 17px; margin: 0 0 10px; color: var(--fg); }
.docs-card ul { list-style: none; margin: 0; padding: 0; }
.docs-card li { margin: 0 0 10px; line-height: 1.4; }
.docs-card li a { font-weight: 600; }
.docs-card li span { display: block; color: var(--fg-3); font-size: 13.5px; }
.docs-pn { display: flex; justify-content: space-between; gap: 16px; margin-top: 48px; max-width: 780px; }
.docs-pn a { flex: 1; border: 1px solid var(--line); border-radius: 12px; padding: 12px 16px; color: var(--fg); font-weight: 600; }
.docs-pn a:hover { border-color: var(--accent); text-decoration: none; }
.docs-pn a span { display: block; color: var(--fg-3); font-weight: 400; font-size: 13px; }
.docs-pn a.next { text-align: right; }
@media (max-width: 1180px) { .docs { grid-template-columns: 230px minmax(0, 1fr); } .docs-toc { display: none; } }
@media (max-width: 860px) {
  .docs { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .docs-side { position: static; max-height: none; padding-top: 20px; border-bottom: 1px solid var(--line); }
  /* On a phone the list would push the page off the screen: the current group only, everything while searching. */
  .docs-side section { display: none; }
  .docs-side section:has(a[aria-current]), .docs-side.finding section { display: block; }
}

/* Blog: a grid of cards, and an article. */
.blog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); gap: 24px; margin-top: 14px; }
.blog-card { display: flex; flex-direction: column; background: color-mix(in srgb, var(--panel) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); border-radius: 18px; overflow: hidden; color: var(--fg); transition: border-color .2s, transform .2s, box-shadow .2s; }
.blog-card:hover { border-color: color-mix(in srgb, var(--accent) 55%, transparent); transform: translateY(-3px); text-decoration: none;
  box-shadow: 0 30px 60px -36px color-mix(in srgb, var(--accent) 60%, transparent); }
.blog-noimg { aspect-ratio: 16 / 10; background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 30%, transparent), color-mix(in srgb, var(--ai) 25%, transparent)); }
.blog-card .blog-meta { color: var(--fg-3); font-size: 13px; margin: 16px 20px 4px; }
.blog-card h2 { font-size: 20px; line-height: 1.3; letter-spacing: -.01em; margin: 0 20px 8px; }
.blog-card p { color: var(--fg-2); font-size: 15px; margin: 0 20px 20px; }
.blog.more { margin-top: 56px; }
.blog.more h2 { font-size: 22px; }
.post { max-width: 900px; }
.post .site-prose { max-width: 760px; }
.post-hero .site-fig { margin-top: 6px; }
@media (prefers-reduced-motion: reduce) { .blog-card { transition: none; } .blog-card:hover { transform: none; } }

/*
 * A screenshot in a window frame (site.js shotFrame): a title bar with the
 * dashboard page's name, then 4:3 of the 16:10 capture anchored right —
 * everything but the app's left menu, so the feature itself is what is big.
 */
.blog-frame { display: block; position: relative; overflow: hidden; border-radius: 14px; background: var(--void);
  border: 1px solid color-mix(in srgb, var(--fg) 13%, transparent);
  box-shadow: 0 40px 80px -40px color-mix(in srgb, var(--void) 95%, transparent), 0 0 0 1px color-mix(in srgb, var(--void) 50%, transparent);
  transition: border-color .25s, box-shadow .25s; }
.blog-chrome { position: relative; z-index: 1; display: flex; align-items: center; gap: 7px; height: 34px; padding: 0 14px;
  background: color-mix(in srgb, var(--panel-2) 70%, var(--panel)); border-bottom: 1px solid color-mix(in srgb, var(--fg) 8%, transparent); }
.blog-chrome i { flex: none; width: 10px; height: 10px; border-radius: 50%; background: color-mix(in srgb, var(--fg) 22%, transparent); }
.blog-chrome span { flex: 0 1 46%; margin: 0 auto; transform: translateX(-27px); min-width: 0; padding: 3px 12px; border-radius: 7px;
  background: color-mix(in srgb, var(--fg) 7%, transparent); color: var(--fg-3); font-size: 12px; line-height: 16px; text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.blog-frame img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; object-position: 100% 0; transition: scale .6s cubic-bezier(.2, .8, .2, 1); }
.blog-frame .blog-noimg { display: block; aspect-ratio: 4 / 3; }

/* Under a post: three more, the same frame, smaller. */
.blog-card .blog-frame { border-radius: 11px; box-shadow: none; }
.blog-card .blog-chrome { height: 26px; gap: 5px; padding: 0 10px; }
.blog-card .blog-chrome i { width: 7px; height: 7px; }
.blog-card .blog-chrome span { font-size: 11px; padding: 1px 10px; transform: translateX(-18px); }
.blog-card { padding: 8px 8px 0; }
.blog-card .blog-meta { margin: 16px 12px 4px; }
.blog-card h2 { margin: 0 12px 8px; }
.blog-card p { margin: 0 12px 20px; }
.blog-card:hover .blog-frame { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/*
 * The blog index: a deck of sheets (site.js postSheet). Each sheet is opaque,
 * so when they stack the one on top hides the rest.
 */
/* The deck breaks out wider than the header, as wide as a whole sheet still fits the screen's height; the intro stays aligned with the header. */
.blog-index { max-width: clamp(1240px, calc((100vh - 353px) * 2.14 + 152px), 1568px); }
.blog-intro { max-width: 1192px; margin: 0 auto; }
.blog-intro { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 0 64px; align-items: end; padding-bottom: 28px; }
.blog-intro .site-kicker { margin-top: 36px; }
.blog-intro .site-lead { margin: 0 0 10px; max-width: 520px; }
.blog-intro h1 { margin: 0; text-wrap: balance; }
.blog-deck { list-style: none; margin: 0; padding: 0; display: grid; gap: 28px; }
/* --self: the sheet's own view timeline, named per sheet in site.js. */
.blog-page { --tone: var(--accent); --top: 92px; --self: none; --next: none; scroll-margin-top: var(--top); }
.blog-sheet { position: relative; isolation: isolate; display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); gap: 56px; align-items: center;
  padding: 24px; border-radius: 26px; color: var(--fg);
  background: radial-gradient(60% 85% at 30% 40%, color-mix(in srgb, var(--tone) 20%, transparent), transparent 72%),
    linear-gradient(180deg, color-mix(in srgb, var(--panel) 96%, var(--fg) 4%), color-mix(in srgb, var(--panel) 70%, var(--bg)));
  border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fg) 9%, transparent), 0 -24px 60px -30px color-mix(in srgb, var(--void) 90%, transparent);
  transition: border-color .25s; }
.blog-sheet:hover { text-decoration: none; border-color: color-mix(in srgb, var(--tone) 40%, transparent); }
.blog-sheet:focus-visible { outline: 2px solid var(--tone); outline-offset: 3px; }
/* The veil a sheet gets as the next one covers it (blog-veil, animated below). */
.blog-sheet::after { content: ""; position: absolute; inset: -1px; z-index: 2; border-radius: inherit; background: var(--bg); opacity: 0; pointer-events: none; }
.blog-page:nth-child(even) .blog-sheet { grid-template-columns: minmax(0, 1fr) minmax(0, 1.65fr);
  background: radial-gradient(60% 85% at 70% 40%, color-mix(in srgb, var(--tone) 20%, transparent), transparent 72%),
    linear-gradient(180deg, color-mix(in srgb, var(--panel) 96%, var(--fg) 4%), color-mix(in srgb, var(--panel) 70%, var(--bg))); }
.blog-page:nth-child(even) .blog-frame { order: 2; }
.blog-sheet:hover .blog-frame { border-color: color-mix(in srgb, var(--tone) 55%, transparent);
  box-shadow: 0 40px 90px -40px color-mix(in srgb, var(--tone) 55%, transparent), 0 0 0 1px color-mix(in srgb, var(--tone) 25%, transparent); }
.blog-sheet:hover .blog-frame img { scale: 1.015; }
.blog-text { display: block; padding: 8px 8px 8px 0; }
.blog-page:nth-child(even) .blog-text { padding: 8px 0 8px 8px; }
.blog-num { display: flex; align-items: center; gap: 12px; font-family: var(--mono); font-size: 13px; font-weight: 700; letter-spacing: .08em; color: var(--tone); }
.blog-num span { color: var(--fg-3); font-weight: 500; margin-left: -8px; }
.blog-num::after { content: ""; flex: 0 0 56px; height: 1px; background: linear-gradient(90deg, var(--tone), transparent); }
.blog-text .blog-meta { display: block; color: var(--fg-3); font-size: 14px; margin: 18px 0 10px; }
.blog-text h2 { font-size: clamp(26px, 2.5vw, 38px); line-height: 1.12; letter-spacing: -.025em; margin: 0 0 16px; font-weight: 750; text-wrap: balance; }
.blog-text p { color: var(--fg-2); font-size: 17px; line-height: 1.6; margin: 0 0 20px; text-wrap: pretty; }
.blog-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 26px; }
.blog-tags span { font-size: 12.5px; line-height: 18px; color: var(--fg-2); padding: 3px 10px; border-radius: 999px; border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent); }
.blog-more { display: inline-flex; align-items: center; gap: 10px; padding: 10px 18px; border-radius: 999px; font-weight: 600; font-size: 15px; color: var(--fg);
  background: color-mix(in srgb, var(--tone) 16%, transparent); border: 1px solid color-mix(in srgb, var(--tone) 45%, transparent); transition: background .2s, border-color .2s; }
.blog-more span { display: inline-block; transition: transform .2s; }
.blog-sheet:hover .blog-more { background: color-mix(in srgb, var(--tone) 28%, transparent); border-color: var(--tone); }
.blog-sheet:hover .blog-more span { transform: translateX(4px); }

/*
 * Scrolling turns the pages, on a screen big enough to hold a sheet: the
 * sheets stick under the header, the next slides up over the last, and the
 * one beneath sinks back and dims while it does. The sink is keyed to the
 * NEXT sheet's own view timeline (named in site.js), from the moment it
 * comes up from the bottom to the moment it lands. Without scroll-driven
 * animations the sheets still stack; with reduced motion, or on a phone,
 * it is a plain list.
 */
@media (min-width: 861px) and (min-height: 620px) and (prefers-reduced-motion: no-preference) {
  .blog-deck { gap: 0; }
  .blog-page { position: sticky; top: var(--top); padding-bottom: 7vh; }
  .blog-page:last-child { padding-bottom: 0; }
  /* The picture never taller than the screen leaves room for. */
  .blog-frame { max-width: calc((100vh - var(--top) - 24px - 48px - 34px) * 4 / 3); justify-self: center; width: 100%; }
  /*
   * Every sheet above stays stuck under the header, so by the seventeenth the
   * browser was holding sixteen full-size layers and stopped drawing them:
   * sheets went see-through and blank. A sheet two beneath the one in view is
   * covered by the one between, so it is hidden (blog-gone, on the timeline
   * of the sheet after next) and costs nothing. The veil animates opacity on
   * the sheet's ::after rather than a custom property, so the compositor
   * runs it without repainting.
   */
  .blog-page { transform-origin: 50% 0; animation-name: blog-sink, blog-gone; animation-timing-function: linear; animation-fill-mode: both; animation-duration: auto;
    animation-range: cover calc((100vh - var(--top)) * .35) cover calc(100vh - var(--top)), cover calc(100vh - var(--top)) cover calc(100vh - var(--top) + 2px); }
  .blog-sheet::after { animation: blog-veil linear both; animation-timeline: var(--next); animation-range: cover calc((100vh - var(--top)) * .35) cover calc(100vh - var(--top)); }
  .blog-page:last-child { animation: none; }
  /* The sheet coming up: its picture settles and its words arrive, done before it lands (its own timeline, --self). */
  .blog-page + .blog-page .blog-frame img { animation: blog-settle linear both; animation-timeline: var(--self); animation-range: cover 0px cover calc(100vh - var(--top)); }
  .blog-page + .blog-page .blog-text { animation: blog-arrive linear both; animation-timeline: var(--self); animation-range: cover calc((100vh - var(--top)) * .2) cover calc((100vh - var(--top)) * .8); }
}
@keyframes blog-sink { to { transform: translateY(-16px) scale(.94); } }
@keyframes blog-veil { to { opacity: .62; } }
@keyframes blog-gone { from { visibility: visible; } to { visibility: hidden; } }
@keyframes blog-settle { from { transform: scale(1.04); } to { transform: none; } }
@keyframes blog-arrive { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: none; } }
/* How far through the deck: a line under the header (the stack's own view timeline). */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    body:has(.blog-stack) { timeline-scope: --deck; }
    .blog-stack { view-timeline: --deck block; }
    body:has(.blog-stack) .site-head::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; transform-origin: 0 50%;
      background: linear-gradient(90deg, var(--accent), var(--ai)); animation: blog-progress linear both; animation-timeline: --deck; animation-range: contain 0% contain 100%; }
  }
}
@keyframes blog-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/*
 * The posts by their short names, in a thin column beside the deck (site.js
 * blogToc) — only where a screen has room for it beside a whole sheet; on
 * anything narrower it is not drawn at all. The one in view: --toc-on, 0 or 1,
 * set by two animations on the sheets' own timelines (--toc-self: this
 * sheet's, --toc-next: the next one's, both named per link in site.js), each
 * switching when its sheet is half way up. A registered number rather than
 * the colour itself, so hover still works.
 */
@property --toc-on { syntax: "<number>"; inherits: true; initial-value: 0; }
.blog-toc { display: none; --toc-self: none; --toc-next: none; }
@media (min-width: 1280px) and (min-height: 620px) {
  .blog-index { max-width: calc(clamp(1240px, calc((100vh - 353px) * 2.14 + 152px), 1568px) + 164px); }
  .blog-stack { display: grid; grid-template-columns: 132px minmax(0, 1fr); column-gap: 32px; align-items: start; }
  /*
   * A rail, not a list (2026-10-27: "fapados"). Every post a dot in its own
   * colour on one thin line; the line fills as the deck is read; the post in
   * view has its dot lit and its name in full weight. All of it always fits:
   * the rows share the height the screen has (never more than 30px each), so
   * there is no scroll bar beside a page that already scrolls.
   */
  .blog-toc { --top: 92px; display: block; position: sticky; top: calc(var(--top) + 26px); }
  .blog-toc ul { position: relative; list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column;
    height: min(calc(var(--n, 24) * 30px), calc(100vh - var(--top) - 64px)); }
  .blog-toc ul::before, .blog-toc ul::after { content: ""; position: absolute; left: 5px; top: 10px; bottom: 10px; width: 2px; border-radius: 2px; }
  .blog-toc ul::before { background: color-mix(in srgb, var(--fg) 10%, transparent); }
  .blog-toc ul::after { background: linear-gradient(180deg, var(--accent), var(--ai)); transform-origin: 50% 0; transform: scaleY(0); }
  .blog-toc li { flex: 1 1 0; min-height: 0; display: flex; }
  .blog-toc a { position: relative; z-index: 1; display: flex; align-items: center; gap: 13px; width: 100%; min-width: 0;
    font-size: 12.5px; line-height: 1.2; letter-spacing: .01em; font-weight: calc(400 + var(--toc-on) * 250);
    color: color-mix(in srgb, var(--fg) calc(var(--toc-on) * 100%), var(--fg-3));
    transition: color .2s; }
  .blog-toc a > span { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; translate: calc(var(--toc-on) * 3px) 0; transition: translate .2s; }
  /* The dot: a ring in the post's colour, filled and haloed when it is the one in view. */
  .blog-toc a::before { content: ""; flex: 0 0 12px; height: 12px; border-radius: 50%; box-sizing: border-box;
    background: color-mix(in srgb, var(--tone) calc(var(--toc-on) * 100%), var(--bg));
    border: 2px solid color-mix(in srgb, var(--tone) calc(45% + var(--toc-on) * 55%), var(--bg));
    box-shadow: 0 0 0 calc(var(--toc-on) * 4px) color-mix(in srgb, var(--tone) 22%, transparent), 0 0 calc(var(--toc-on) * 14px) color-mix(in srgb, var(--tone) 60%, transparent);
    scale: calc(.72 + var(--toc-on) * .28); transition: scale .2s; }
  /* The post in view on a filled pill of its own colour (asked for on 2026-10-27), dot and name inside it. */
  .blog-toc a::after { content: ""; position: absolute; z-index: -1; inset: 2px -10px 2px -7px; border-radius: 9px;
    background: color-mix(in srgb, var(--tone) calc(var(--toc-on) * 30%), transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tone) calc(var(--toc-on) * 45%), transparent); }
  .blog-toc a:hover { color: var(--fg); text-decoration: none; }
  .blog-toc a:hover::before { scale: 1; border-color: var(--tone); }
  .blog-toc a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
  @supports (animation-timeline: view()) {
    .blog-toc a { animation-name: blog-toc-on, blog-toc-off; animation-duration: auto; animation-timing-function: linear; animation-fill-mode: forwards;
      animation-timeline: var(--toc-self), var(--toc-next); animation-range: cover calc((100vh - var(--top)) * .5) cover calc((100vh - var(--top)) * .5 + 1px); }
    /* The first starts on and only goes off; the last only comes on. (An animation on timeline none counts as finished, and would apply.) */
    .blog-toc li:first-child a { --toc-on: 1; animation-name: blog-toc-off; animation-timeline: var(--toc-next); }
    .blog-toc li:last-child a { animation-name: blog-toc-on; animation-timeline: var(--toc-self); }
    .blog-toc li:only-child a { animation: none; }
    /* How far through the deck: the rail fills (the stack's own timeline, --deck). */
    @media (prefers-reduced-motion: no-preference) {
      .blog-toc ul::after { animation: blog-toc-fill linear both; animation-timeline: --deck; animation-range: contain 0% contain 100%; }
    }
  }
}
/*
 * On a wide screen the rail leaves the deck alone (asked for on 2026-10-28,
 * "not so fit to the screen"): the sheets take their full width again and the
 * rail stands in the middle of the empty margin to their left, still sticky,
 * rather than squeezing a column out of the deck. The nav spans the deck's
 * height so the rail inside it can stick and stop with the deck.
 */
@media (min-width: 2000px) and (min-height: 620px) {
  .blog-index { max-width: clamp(1240px, calc((100vh - 353px) * 2.14 + 152px), 1568px); }
  .blog-stack { display: block; position: relative; }
  .blog-toc { position: absolute; top: 0; bottom: 0; width: 150px; right: calc(100% + max(32px, ((100vw - 100%) / 2 - 150px) / 2)); }
  .blog-toc ul { position: sticky; top: calc(var(--top) + 26px); }
}
@keyframes blog-toc-fill { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes blog-toc-on { to { --toc-on: 1; } }
@keyframes blog-toc-off { to { --toc-on: 0; } }
@media (max-width: 1100px) {
  .blog-sheet, .blog-page:nth-child(even) .blog-sheet { gap: 36px; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
  .blog-page:nth-child(even) .blog-sheet { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); }
  .blog-text h2 { font-size: 26px; }
  /* The words never taller than the picture, so every sheet is the same height and covers the last. */
  .blog-text p { font-size: 16px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; overflow: hidden; }
  .blog-tags { display: none; }
}
@media (max-width: 860px) {
  .blog-intro { grid-template-columns: minmax(0, 1fr); }
  .blog-intro h1 { margin-bottom: 12px; }
  .blog-deck { gap: 18px; }
  /* A phone: one column, the picture first, the words under it. */
  .blog-sheet, .blog-page:nth-child(even) .blog-sheet { grid-template-columns: minmax(0, 1fr); gap: 4px; padding: 10px; border-radius: 20px;
    background: radial-gradient(90% 50% at 50% 20%, color-mix(in srgb, var(--tone) 18%, transparent), transparent 75%),
      linear-gradient(180deg, color-mix(in srgb, var(--panel) 96%, var(--fg) 4%), color-mix(in srgb, var(--panel) 70%, var(--bg))); }
  .blog-page:nth-child(even) .blog-frame { order: 0; }
  .blog-frame { border-radius: 12px; }
  .blog-chrome { height: 28px; padding: 0 10px; gap: 5px; }
  .blog-chrome i { width: 8px; height: 8px; }
  .blog-chrome span { font-size: 11px; padding: 1px 10px; transform: translateX(-20px); }
  .blog-text, .blog-page:nth-child(even) .blog-text { padding: 14px 8px 10px; }
  .blog-text .blog-meta { margin: 12px 0 6px; font-size: 13px; }
  .blog-text h2 { font-size: 22px; line-height: 1.2; margin-bottom: 10px; }
  .blog-text p { font-size: 15.5px; line-height: 1.55; margin-bottom: 14px; display: block; -webkit-line-clamp: none; }
  .blog-tags { display: none; }
  .blog-more { padding: 8px 14px; font-size: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .blog-frame, .blog-frame img, .blog-more, .blog-more span, .blog-sheet { transition: none; }
  .blog-sheet:hover .blog-frame img { scale: none; }
}

.site-ctabox { display: flex; flex-wrap: wrap; gap: 18px 32px; align-items: center; justify-content: space-between; margin: 52px 0 0; padding: 26px 30px; max-width: 900px;
  border-radius: 18px; border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  background: linear-gradient(120deg, color-mix(in srgb, var(--accent) 18%, transparent), color-mix(in srgb, var(--ai) 14%, transparent)); }
.site-ctabox h2 { margin: 0 0 4px; font-size: 22px; color: var(--fg); }
.site-ctabox p { margin: 0; color: var(--fg-2); }
.site-ctabtns { display: flex; gap: 12px; flex-wrap: wrap; }

/* Quickstart: a clock down the left, a step and its picture on the right. */
.qs-go { display: flex; gap: 12px; flex-wrap: wrap; margin: 0 0 40px; }
.qs-steps { list-style: none; margin: 0; padding: 0; max-width: 980px; }
.qs-step { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 22px; position: relative; padding-bottom: 34px; }
.qs-step::before { content: ""; position: absolute; left: 41px; top: 44px; bottom: 0; width: 2px;
  background: linear-gradient(var(--accent), color-mix(in srgb, var(--accent) 10%, transparent)); }
.qs-step:last-child::before { display: none; }
.qs-time span { display: inline-flex; align-items: center; justify-content: center; width: 84px; height: 40px; border-radius: 20px;
  font-family: var(--mono); font-weight: 700; font-size: 15px; color: var(--accent-ink); background: var(--accent); position: relative; z-index: 1; }
.qs-body h2 { display: flex; align-items: center; gap: 12px; font-size: 24px; letter-spacing: -.015em; margin: 4px 0 10px; color: var(--fg); scroll-margin-top: 84px; }
.qs-n { color: var(--fg-3); font-size: 15px; font-weight: 600; }
.qs-body p { color: var(--fg-2); max-width: 720px; margin: 0 0 6px; }
.qs-body code { font-family: var(--mono); font-size: .86em; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; color: var(--fg); }
.qs-body .site-fig { margin: 18px 0 0; }

.site-404 { padding-bottom: 40px; }
.site-foot { border-top: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); padding: 36px 0 44px; color: var(--fg-3); font-size: 14px; }
.site-footin { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto; gap: 24px; align-items: start; }
.site-foot p { margin: 10px 0 0; max-width: 420px; }
.site-foot nav { display: flex; flex-wrap: wrap; gap: 10px 20px; }
.site-foot nav a { color: var(--fg-2); }
.site-copy { margin: 0; }
/* What the front page says to a crawler with no script (site.js frontHead). */
.site-pre { max-width: 680px; margin: 48px auto; padding: 0 20px; }

@media (max-width: 760px) {
  .site-headin { gap: 14px; flex-wrap: wrap; padding-top: 10px; padding-bottom: 10px; }
  .site-nav { order: 3; flex-basis: 100%; gap: 18px; }
  .site-cta { margin-left: auto; }
  .site-wrap { padding: 0 16px; }
  .site-lead { font-size: 17px; }
  .qs-step { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  .qs-step::before { display: none; }
  .qs-time span { width: auto; padding: 0 14px; height: 32px; font-size: 14px; }
  .site-footin { grid-template-columns: minmax(0, 1fr); }
  .site-ctabox { padding: 22px 20px; }
}

/* All apps: Help — the manual, which opens beside the dashboard. */
.g-help { --g: var(--info); }
/* The install administrator's Licences, and an organisation's own Plan & licence. */
.g-admin { --g: var(--serious); }
.g-account { --g: var(--s3); }

/* The landing's foot: the rest of the public site, by real address (server/site.js). */
.landing-links { position: relative; z-index: 1; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 22px; margin: 28px auto 36px; font-size: 14px; }
.landing-links a { color: var(--fg-2); }
.landing-links a:hover { color: var(--fg); }
.cta-live a { color: var(--fg-2); text-decoration: underline; text-underline-offset: 3px; }

/* The front page as the server drew it (site.js frontBody): shown until the app takes over — unless this browser was signed in last time. */
.ssr-wait { display: none; }
html.was-in .ssr-landing { display: none; }
html.was-in .ssr-wait { display: flex; }
.ssr-ways { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 14px; }

/* A tile leads to the page about it (landing.js LF_LINK): the whole card is the link, "Read more" says so. */
.lf-card > a.lf-go { position: static; display: inline-flex; gap: 6px; margin-top: 14px; font-size: 13px; font-weight: 600; color: var(--tone); }
.lf-card > a.lf-go::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.lf-card > a.lf-go:hover { text-decoration: none; }
.lf-card:has(a.lf-go) { cursor: pointer; }
.lf-card > a.lf-go:focus-visible { outline: none; }
.lf-card:has(a.lf-go:focus-visible) { outline: 2px solid var(--tone); outline-offset: 2px; }

/*
 * The manual's menu (site.js docsSidebar): where to start set apart, groups
 * with their own mark and colour, folded; the page being read opened into
 * its chapters. Two columns now — the chapters live in the menu.
 */
.docs { grid-template-columns: 290px minmax(0, 1fr); gap: 52px; }
.docs-side { padding: 28px 8px 32px 0; font-size: 14.5px; scrollbar-width: thin; }
.docs-all > summary { display: none; }
.docs-find { margin-bottom: 14px; }
.docs-feat { display: grid; gap: 8px; margin-bottom: 18px; }
.docs-feat a { display: block; padding: 10px 14px; border-radius: 12px; color: var(--fg);
  background: linear-gradient(120deg, color-mix(in srgb, var(--g) 22%, transparent), color-mix(in srgb, var(--g) 6%, transparent));
  border: 1px solid color-mix(in srgb, var(--g) 45%, transparent); }
.docs-feat a:hover { text-decoration: none; border-color: var(--g); }
.docs-feat a[aria-current] { box-shadow: inset 3px 0 0 var(--g); }
.docs-feat b { display: block; font-size: 15px; }
.docs-feat span { display: block; color: var(--fg-3); font-size: 12.5px; }
.docs-grps { display: grid; gap: 4px; }
.docs-grp > summary, .docs-grp .docs-solo { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: 10px;
  cursor: pointer; list-style: none; color: var(--fg); font-weight: 600; user-select: none; }
.docs-grp > summary::-webkit-details-marker { display: none; }
.docs-grp > summary:hover, .docs-grp .docs-solo:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); text-decoration: none; }
.docs-gi { flex: 0 0 auto; width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center;
  color: var(--g); background: color-mix(in srgb, var(--g) 16%, transparent); }
.docs-gi svg { width: 16px; height: 16px; }
.docs-gt { flex: 1; min-width: 0; line-height: 1.25; }
.docs-gt small { display: block; font-weight: 400; color: var(--fg-3); font-size: 12.5px; }
.docs-gn { font-size: 11.5px; font-weight: 600; color: var(--fg-3); background: color-mix(in srgb, var(--fg) 8%, transparent);
  border-radius: 999px; padding: 1px 8px; }
.docs-grp > summary::after { content: ""; width: 7px; height: 7px; border-right: 1.6px solid var(--fg-3); border-bottom: 1.6px solid var(--fg-3);
  transform: rotate(-45deg); transition: transform .15s; margin: 0 4px 0 2px; }
.docs-grp[open] > summary::after { transform: rotate(45deg); }
.docs-grp.mine > summary .docs-gt, .docs-grp.solo.mine .docs-gt { color: var(--g); }
.docs-grp > ul { list-style: none; margin: 2px 0 8px 22px; padding: 0 0 0 14px; border-left: 1px solid color-mix(in srgb, var(--g) 30%, var(--line)); }
.docs-grp.solo > ul { margin: 0; padding: 0; border: 0; }
.docs-grp li a { display: block; padding: 5px 10px; border-radius: 8px; color: var(--fg-2); line-height: 1.35; }
.docs-grp li a:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); text-decoration: none; }
.docs-grp li.on > a { color: var(--fg); font-weight: 650; background: color-mix(in srgb, var(--g) 16%, transparent); box-shadow: inset 3px 0 0 var(--g); }
.docs-grp.solo li.on > a { box-shadow: none; }
.docs-sub { list-style: none; margin: 4px 0 6px 10px; padding: 0 0 0 10px; border-left: 1px dashed var(--line-2); }
.docs-sub a { display: block; padding: 3px 8px; font-size: 13.5px; color: var(--fg-3); border-radius: 6px; line-height: 1.35; }
.docs-sub a:hover { color: var(--fg); text-decoration: none; }
.docs-sub a.here { color: var(--g); font-weight: 600; }
.docs-grp.solo .docs-sub { margin-left: 46px; }
@media (prefers-reduced-motion: reduce) { .docs-grp > summary::after { transition: none; } }

/* The index: a card per group, with its mark and what is in it. */
.docs-card { border-top: 3px solid var(--g); }
.docs-card h2 { display: flex; align-items: center; gap: 10px; }
.docs-blurb { color: var(--fg-3); font-size: 14px; margin: 0 0 12px; }

@media (max-width: 1180px) { .docs { grid-template-columns: 250px minmax(0, 1fr); gap: 36px; } }
@media (max-width: 860px) {
  .docs { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .docs-side { padding: 16px 0 12px; }
  .docs-all > summary.docs-allbtn { display: flex; align-items: center; justify-content: space-between; padding: 11px 14px; border-radius: 12px;
    border: 1px solid var(--line); background: var(--panel); color: var(--fg); font-weight: 600; cursor: pointer; list-style: none; }
  .docs-all > summary.docs-allbtn::-webkit-details-marker { display: none; }
  .docs-all > summary.docs-allbtn::after { content: "▾"; color: var(--fg-3); }
  .docs-all[open] > summary.docs-allbtn { margin-bottom: 12px; }
  .docs-all[open] > summary.docs-allbtn::after { content: "▴"; }
}

/*
 * Locked out by the plan (app.js lockedView): Plan & licence and the
 * account's basics, centred where the page would be.
 */
.locked-main .locked-page { max-width: 880px; margin: 0 auto; padding: 24px 16px; display: grid; gap: 14px; }

/*
 * Licences (app.js licencesView): tiles that filter, a table with a header
 * that stays, and a side panel that becomes the whole screen on a phone.
 */
.licpage .lictile { text-align: left; font: inherit; color: inherit; cursor: pointer; }
.licpage .lictile.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.licpage .tablewrap thead th { position: sticky; top: 0; z-index: 1; background: var(--panel); }
.licpage tr.licrow.on td { background: var(--accent-bg); }
.licurg.bad { color: var(--bad); font-weight: 600; }
.licurg.warn { color: var(--warn); }
.licurg.muted { color: var(--fg-3); }
.licdrawer-wrap { position: fixed; inset: 0; z-index: 60; display: flex; justify-content: flex-end; }
.licscrim { position: absolute; inset: 0; background: color-mix(in srgb, var(--void) 45%, transparent); }
.licdrawer { position: relative; width: min(560px, 100%); height: 100%; overflow-y: auto; background: var(--panel);
  border-left: 1px solid var(--line); box-shadow: var(--shadow-lg); padding: 18px 20px 28px; }
.licdrawer-head { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; }
.licdrawer .licsec { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); margin: 18px 0 6px; }
.licdrawer .licform { border: 1px solid var(--line); border-radius: 8px; padding: 12px; margin-top: 12px; background: var(--panel-2); }
@media (max-width: 700px) {
  .licdrawer { width: 100%; border-left: 0; padding: 14px 16px 24px; }
}

/* Licences → Usage & pricing (usage.js): the settings form's fields, two to four a row. */
.pricegrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); gap: 10px 18px; }
.pricefield { display: flex; flex-direction: column; gap: 4px; margin: 0; font-size: 13px; }
.pricefield > span:first-child { color: var(--fg-2); }

/* Host settings: names strangers asked this machine for — folded, quiet, not a problem (app.js hostSettingsView). */
.askedsites > summary { cursor: pointer; list-style: none; }
.askedsites > summary::-webkit-details-marker { display: none; }
.askedsites > summary::before { content: "▸ "; }
.askedsites[open] > summary::before { content: "▾ "; }
.askedsites > summary span { color: var(--fg-3); }
.askedlist { display: flex; flex-wrap: wrap; gap: 4px 14px; max-height: 260px; overflow-y: auto; font-size: 12px; color: var(--fg-3); }
.askedname a { font-size: 11px; }

/* Home, all organisations: whose is what — one quiet tile each, a click switches (app.js homeOrgStrip). */
.homeorgs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 190px), 1fr)); gap: 10px; margin-bottom: 14px; }
.homeorg { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; text-align: left; padding: 10px 14px; border-radius: 10px;
  background: var(--panel); border: 1px solid var(--line); color: var(--fg); cursor: pointer; font: inherit; }
.homeorg:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); background: var(--panel-2); }
.homeorg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.homeorg b { font-size: 14px; }

/* obscenter.site measuring its own visits only after "Allow" (server/selftag.js). */
.consentbar { position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 9999; max-width: 760px; margin: 0 auto;
  display: flex; gap: 14px; align-items: center; flex-wrap: wrap; padding: 12px 16px; border-radius: 10px;
  background: var(--panel); color: var(--fg); border: 1px solid var(--line); box-shadow: 0 6px 24px rgba(0,0,0,.35); font-size: 13.5px; line-height: 1.45; }
.consentbar p { margin: 0; flex: 1 1 320px; color: var(--fg-2); }
.consentbar span { display: flex; gap: 8px; margin-left: auto; }
.consentbar button { font: inherit; padding: 6px 14px; border-radius: 7px; border: 1px solid var(--line); background: transparent; color: var(--fg); cursor: pointer; }
.consentbar button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.privpage .linkbtn { font: inherit; padding: 0; border: 0; background: none; color: var(--accent); cursor: pointer; text-decoration: underline; }

/* Registration: the two boxes that make the terms bind (server/terms.js). */
.accept-box { margin-top: 14px; display: grid; gap: 10px; }
.landing-form label.accept, .accept-box label.accept { display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: start; font-size: 12.5px; line-height: 1.45; color: var(--fg-2); font-weight: 400; margin: 0; }
.landing-form label.accept input, .accept-box label.accept input { width: 16px; height: 16px; margin: 2px 0 0; }
.oauth-btn.is-off { opacity: .45; }

/* ===================================================== page layout system ===
 *
 * The building blocks every data page is made of (public/layout.js builds
 * them; .claude/skills/observai/references/layout.md is the rulebook;
 * `npm run test:layoutfill` measures the pages that use them).
 *
 * Three widths, taken from what was already true here rather than chosen:
 *
 *   phone    <= 800px  the sidebar becomes a drawer at 800 and every existing
 *                      single-column rule (.grid, .row wrapping, KPI pairs)
 *                      switches there; a second phone breakpoint at 720 would
 *                      leave 720-800 with a drawer and desktop grids.
 *   desktop  801-1799  content is 525-1523px beside the 236px sidebar. Chart
 *                      cards go two-up from 1100 (content >= 824px, two charts
 *                      of ~400px — the floor the .trio note measured for a
 *                      readable 24h chart is ~380px).
 *   wide    >= 1800    content >= 1524px: three charts of ~490px, a detail page
 *                      gets its side column (main >= 1100px beside a 400px
 *                      side). Between the existing 1700 (Hosts' top row) and
 *                      1980 (.trio) steps.
 *
 * The content fills the width at every size: no max-width on a page or a grid.
 * Running text keeps its 90ch measure (.content p). Spacing is one value,
 * --ly-gap, between every block and every card.
 */
:root { --ly-gap: 14px; }

/* The page owns the vertical rhythm; a block's own margin does not add to it. */
.ly-page { display: flex; flex-direction: column; gap: var(--ly-gap); min-width: 0; }
.ly-page > *, .ly-main > *, .ly-side > * { margin-top: 0 !important; margin-bottom: 0 !important; min-width: 0; }
/* The blocks own their gaps too: a reused card's inline margin-bottom on top
   of a grid's gap made Traces → Routes 28px apart where every other pair is 14. */
.ly-pair > *, .ly-charts > *, .ly-band > *, .ly-kpis > * { margin-top: 0 !important; margin-bottom: 0 !important; }

/* Head: identity left, controls right, one line where it fits. */
.ly-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; min-height: 32px; }
.ly-head-l, .ly-head-r { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; min-width: 0; }
.ly-head-l { flex: 1 1 auto; }
.ly-head-r { flex: 0 1 auto; justify-content: flex-end; margin-left: auto; }
.ly-head .ly-title { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }

/* A group heading inside a page: quieter than a card heading, not a box. */
.ly-sect { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13px;
  color: var(--fg-2); padding-top: 6px; }
.ly-sect b { color: var(--fg); font-weight: 650; }

/* KPI row: equal tiles on one line on a desktop (eight fit a 1440 screen),
   pairs on a phone, and an odd last tile spans its pair rather than standing alone. */
.ly-kpis { display: grid; gap: var(--ly-gap); grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr)); }
.ly-kpis > .kpi { min-width: 0; }
@media (max-width: 800px) {
  .ly-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .ly-kpis > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* Chart grid: 1 / 2 / 3 columns; never one card alone in a row. */
.ly-charts { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: stretch; }
.ly-charts > * { min-width: 0; }
@media (min-width: 1100px) {
  .ly-charts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ly-charts > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (min-width: 1800px) {
  .ly-charts:not(.ly-max2):not(.ly-main *) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ly-charts:not(.ly-max2):not(.ly-main *) > :last-child:nth-child(odd) { grid-column: auto; }
  .ly-charts:not(.ly-max2):not(.ly-main *) > :last-child:nth-child(3n+1) { grid-column: 1 / -1; }
  .ly-charts:not(.ly-max2):not(.ly-main *) > :last-child:nth-child(3n+2) { grid-column: span 2; }
  /* Two or four charts read better as halves and quarters than as 3 + 1. */
  .ly-charts:not(.ly-max2):not(.ly-main *):has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ly-charts:not(.ly-max2):not(.ly-main *):has(> :nth-child(2):last-child) > :last-child { grid-column: auto; }
  .ly-charts:not(.ly-max2):not(.ly-main *):has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .ly-charts:not(.ly-max2):not(.ly-main *):has(> :nth-child(4):last-child) > :last-child { grid-column: auto; }
}
/* Beside a detail page's side column the main column is ~380-520px narrower,
   so its chart grid goes three-up only from 2300px (main >= 1500px). */
@media (min-width: 2300px) {
  .ly-main .ly-charts:not(.ly-max2) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ly-main .ly-charts:not(.ly-max2) > :last-child:nth-child(odd) { grid-column: auto; }
  .ly-main .ly-charts:not(.ly-max2) > :last-child:nth-child(3n+1) { grid-column: 1 / -1; }
  .ly-main .ly-charts:not(.ly-max2) > :last-child:nth-child(3n+2) { grid-column: span 2; }
  /* Two or four charts read better as halves and quarters than as 3 + 1. */
  .ly-main .ly-charts:not(.ly-max2):has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ly-main .ly-charts:not(.ly-max2):has(> :nth-child(2):last-child) > :last-child { grid-column: auto; }
  .ly-main .ly-charts:not(.ly-max2):has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .ly-main .ly-charts:not(.ly-max2):has(> :nth-child(4):last-child) > :last-child { grid-column: auto; }
}

/* Cards side by side that are not charts (two short tables, a list beside a
   chart): halves from 1100, and packed columns, so a short card leaves no hole. */
.ly-pair { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: stretch; }
@media (min-width: 1100px) {
  .ly-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ly-pair > :only-child { grid-column: 1 / -1; }
  .ly-pair > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
/* cardPair(…, { from: 'wide' }): abreast only from 1800px — a chart beside the
   ten-row table it explains, two eight-column tables (at 1440 a half is ~575px
   and the table would scroll inside it). */
@media (min-width: 1100px) and (max-width: 1799px) { .ly-pair.ly-pair-w { grid-template-columns: minmax(0, 1fr); } }

/* Table card: the whole width; a phone hides the low-priority columns. */
.ly-table { min-width: 0; }
@media (max-width: 1100px) { .ly-page .col-p3 { display: none; } }
@media (max-width: 800px) { .ly-page .col-p2 { display: none; } }

/* Detail page: main + side on a wide screen, stacked below that. */
.ly-detail { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: start; }
.ly-main, .ly-side { display: flex; flex-direction: column; gap: var(--ly-gap); min-width: 0; }
.ly-side:empty { display: none; }
@media (min-width: 1800px) {
  .ly-detail { grid-template-columns: minmax(0, 1fr) clamp(380px, 24%, 520px); }

}

/* Empty state: what is missing, why, and what to do — centred in its card. */
.ly-empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; padding: 32px 20px; }
.ly-empty b { font-size: 14px; }
.ly-empty p { margin: 0; max-width: 60ch; }
.ly-empty-act { margin-top: 6px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
/* Three related charts on one time axis (a service's requests, errors and
   latency) stay three abreast from 1400px — content 1124px, ~365px each — so
   the same moment can be read across all three. */
@media (min-width: 1400px) {
  .ly-charts.ly-trio { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ly-charts.ly-trio > :last-child:nth-child(odd) { grid-column: auto; }
}

/* Packed columns for cards of uneven height (a tall table beside two short
   cards): the next card starts where the one above it ended, so nothing
   leaves a hole. Two columns from 1100px. Cards are never split across columns.
 *
 * Each card is an inline-block line of its own (width 100%, top-aligned), not
 * a block. A block's bottom margin is TRUNCATED where a column breaks after
 * it (CSS fragmentation), so the card ending a column that is not the last
 * lost its gap; when that column was the tallest, the flow's box ended at the
 * card and the pull below (-gap) laid the next block flush against it — 0px,
 * measured at 2560 on Mobile app and the server pages. A line box is never
 * cut: its height includes the inline-block's margin, so every column ends
 * one gap below its last card, and the pull takes exactly that gap back.
 * orphans/widows 1: they count lines, and here a line is a card — the default
 * 2 would stop the balancer from leaving one card at a column's top or foot. */
.ly-flow { column-gap: var(--ly-gap); orphans: 1; widows: 1; }
.ly-flow > * { break-inside: avoid; display: inline-block; vertical-align: top; width: 100%; margin: 0 0 var(--ly-gap) !important; }
.ly-page > .ly-flow, .ly-main > .ly-flow, .ly-side > .ly-flow { margin-bottom: calc(-1 * var(--ly-gap)) !important; }
@media (min-width: 1100px) {
  .ly-flow { column-count: 2; }
  .ly-flow:has(> :only-child) { column-count: 1; }
}
/* The start of a column, placed by layout.js (lyBalance): each card goes to
   the shortest column, so one tall card no longer leaves a column 400px short. */
.ly-flow > .ly-colbreak { display: block; width: 100%; height: 0; margin: 0 !important; padding: 0; break-before: column; }
/* Cards holding six-column tables pair up only on a wide screen (.ly-flow-w):
   at 1440 a half is ~575px and the table would scroll inside it. */
@media (min-width: 1100px) and (max-width: 1799px) { .ly-flow.ly-flow-w { column-count: 1; } }
/* Three columns only where the page asks (.ly-flow3) and has the cards to fill
   them: the balancer cannot split a card, so three uneven cards in three
   columns can leave one column empty (measured on a service page at 2560). */
@media (min-width: 2300px) {
  /* Counted in cards (the column breaks lyBalance adds are not cards); the balancer fills three from four. */
  .ly-flow.ly-flow3:has(> :nth-child(4 of :not(.ly-colbreak))) { column-count: 3; }
}

/* Inside one card: a figure beside the numbers that explain it (a chart and
   its breakdown, a map and its table). Side by side from 1100px. */
.ly-split { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr); align-items: start; }
@media (min-width: 1100px) { .ly-split { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); } }
.ly-split > * { min-width: 0; }

/* Head children: the row owns the spacing, a reused block's own margin does not count. */
.ly-head-l > *, .ly-head-r > * { margin: 0 !important; }

/* KPI band: the stat tiles beside the one chart they summarise, from 1100px —
   two full-width rows (four 570px tiles, then a 10:1 chart ribbon) become one.
   The tiles go two by two and stretch to the chart's height. */
.ly-band { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); }
.ly-band > * { min-width: 0; }
@media (min-width: 1100px) {
  .ly-band { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); align-items: stretch; }
  .ly-band > .ly-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: stretch; }
  .ly-band > .ly-kpis > .kpi { display: flex; flex-direction: column; justify-content: center; }
}
@media (min-width: 1800px) { .ly-band { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); } }
/* An odd number of tiles beside the chart: the last one spans the pair, so
   five tiles read 2 + 2 + 1 wide, never a hole beside the fifth. */
@media (min-width: 1100px) { .ly-band > .ly-kpis > :last-child:nth-child(odd) { grid-column: 1 / -1; } }
/* A column that takes the rest of the row and truncates (a log line): the
   table never pushes the columns after it out of the card. */
.ly-page td.ly-fill { width: 100%; max-width: 0; }
/* Inside it, a .truncate box cuts at the column's edge, not at its own 380px
   (".truncate" above): an error message, a statement, a command line stopped a
   third of the way across a 2560 card with the rest of the column empty. */
.ly-page td.ly-fill .truncate { max-width: none; }
/* A link that is itself the truncating box: on a phone the visible part is
   the whole link, never a link cut off inside a clipping wrapper (test:mobile). */
a.cellink { display: block; }
/* Beside a filling column the others keep to one line: given width:100%, the
   fill column would otherwise squeeze a date onto two lines in every row. */
.ly-page table:has(td.ly-fill) td:not(.ly-fill) { white-space: nowrap; }
/* ...except a cell that is prose (a description beside the column that fills):
   column `cls: 'ly-wrap'` wraps at the column's edge instead of pushing the
   table past the card (the API catalogue: 3472px of one-line descriptions). */
.ly-page table:has(td.ly-fill) td.ly-wrap, td.ly-wrap { white-space: normal; overflow-wrap: break-word; min-width: 16ch; }

/*
 * A table fits its card on a desktop and a wide screen. `max-content` (above)
 * never wraps a cell, so a table one description too wide pushed its last
 * columns behind a sideways scroll nobody knew was there — Accounts' "May", a
 * service's p50/p95, a sensor's last reading, measured at 1440 and 2560. At
 * 100% the browser's own table layout wraps the cells that have words to wrap
 * first; a table that still does not fit hides its `prio: 3` columns (below
 * 1100px) and only then scrolls. `test:layoutfill` fails on any table wider
 * than its box above 800px (`.scrollx` marks a box that scrolls on purpose).
 * A phone keeps max-content and its sideways scroll: there the low-priority
 * columns are hidden instead, and the sorted column must stay on the screen.
 */
/* A chart card's heading is one line in a row of charts: a long grey note
   cuts at the card's edge instead of pushing the pin onto a second line —
   which made one chart of a row 30px shorter than its neighbours. */
@media (min-width: 1100px) {
  .ly-charts > .card > h3 { flex-wrap: nowrap; min-width: 0; }
  .ly-charts > .card > h3 > .drill { display: block; min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ly-charts > .card > h3 > :not(.drill):not(.spacer) { flex: none; }
}
/*
 * A chart card that takes its row's height (a chart beside the table or list
 * it explains): `.ly-fillchart` on the card, `fill: true` on the timeChart.
 * The card is a column, the chart's container takes what is left under the
 * heading, and the chart redraws at that height (charts.js, opts.fill) —
 * instead of a fixed-height chart at the top of a card twice its height.
 */
.card.ly-fillchart { display: flex; flex-direction: column; }
.card.ly-fillchart > :has(.ly-fillplot), .card.ly-fillchart > * > :has(> .ly-fillplot) {
  flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* Short lists side by side INSIDE one card (a breakdown by browser, system,
   page): columns that end where their lists do, never three stretched cards. */
.ly-cols3 { display: grid; gap: 14px 28px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 801px) { .ly-cols3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ly-cols3 > :last-child:nth-child(odd) { grid-column: 1 / -1; } }
@media (min-width: 1100px) { .ly-cols3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .ly-cols3 > :last-child:nth-child(odd) { grid-column: auto; } }
.ly-sub { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); margin: 0 0 8px; }
/* A card that is one line (a name on the left, a small form on the right). */
.card.ly-bar > h3 { margin: 0; row-gap: 10px; }
.card.ly-bar > h3 > .row { flex: 1 1 560px; justify-content: flex-end; font-weight: 400; }
.card.ly-bar > h3 > .row > .muted { display: none; }
@media (max-width: 800px) { .card.ly-bar > h3 > .row { flex: 1 1 100%; justify-content: flex-start; } }
/* A monospace header name in a settings note may break on a phone: kept on one
   line it ran 8px out of its card and the page scrolled sideways (test:mobile,
   App settings at 390). */
@media (max-width: 800px) { .ly-set-note .mono { white-space: normal; overflow-wrap: anywhere; } }
/* The API catalogue: every group's columns on the same lines, so nine tables read as one. */
@media (min-width: 801px) {
  .apicat table { table-layout: fixed; }
  .apicat th:nth-child(1) { width: 56%; }
  .apicat th:nth-child(2) { width: 22%; }
}
/* A session's device: two short lines, cut at a readable width. */
.ss-list .ss-dev { max-width: 230px; }
/* A path cut in the middle (app.js pathParts): the directories shrink to an
   ellipsis first, the last segment stays whole while it fits. */
.pth { display: flex; min-width: 0; white-space: nowrap; }
.pth > .pth-a { flex: none; }
.pth > .pth-h { flex: 0 1 auto; min-width: 1.5ch; overflow: hidden; text-overflow: ellipsis; }
.pth > .pth-t { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.pth > .pth-h + .pth-t { flex-shrink: 0; max-width: calc(100% - 2ch); }
/* A service's callers from one to three countries: one line, a figure per country. */
.svcgeo-cs { display: inline-flex; flex-wrap: wrap; gap: 6px 22px; font-weight: 400; font-size: 12.5px; }
.svcgeo-c b { color: var(--fg); font-weight: 600; margin-right: 6px; }
/* A device's resolution choices: one group that wraps as a whole. */
.iotres { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
@media (min-width: 801px) { .iotres { flex-wrap: nowrap; } }
/* A flow in a detail page's side column: two abreast while the side follows
   the main column (below 1800px), one column once it stands beside it. */
@media (min-width: 1800px) { .ly-side .ly-flow { column-count: 1; } }
/* On a phone a truncating cell (a route, a path, a name) gives up its 380px
   so the figures after it — the column the table is sorted by — stay on the
   screen: "Calls" began 43px past a 328px box on a service's routes. */
@media (max-width: 800px) {
  .ly-page .tablewrap .truncate { max-width: min(380px, 46vw); }
  /* Tighter cells and no idle sort arrows (a tap on the heading still sorts):
     the ~60px a 328px box needs for its sorted column to stay on the screen. */
  .ly-page .tablewrap:not(.stacked) th, .ly-page .tablewrap:not(.stacked) td { padding-left: 7px; padding-right: 7px; }
  .ly-page .tablewrap:not(.stacked) th { letter-spacing: .03em; }
  .ly-page .tablewrap:not(.stacked) th:not(.sorted) .sort-arrow { display: none; }
  /* A table with a prose column (ly-wrap) fits the phone and wraps it, rather
     than laying a description out on one line and scrolling to its end. */
  .tablewrap:has(td.ly-wrap) table { width: 100%; }
  /* A table with a column that takes the rest and truncates (ly-fill) fits the
     phone too: that column gives way, the figures after it stay on the screen. */
  .ly-page .tablewrap:has(td.ly-fill) table { width: 100%; }
  /* ...and its heading gives way as well ("Events and actions" was 130px of a 328px row). */
  .ly-page th.ly-fill { max-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .tablewrap td.ly-wrap { min-width: 0; }
}
@media (min-width: 801px) {
  .tablewrap table { width: 100%; }
  .tablewrap td:not(.num) { overflow-wrap: break-word; }
  .tablewrap td .badge, .tablewrap td .chip, .tablewrap td .btn, .tablewrap td time { overflow-wrap: normal; }
}

/*
 * The head's buttons are one height, the height of a field: 37px links, 28px
 * small buttons and 21px chips stood in one line of heads across the pages
 * (measured at 1440). Same box as the settings rows' buttons (32px).
 */
.ly-head .btn, .ly-head-r .btn, .ly-sect .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 32px; box-sizing: border-box; padding: 0 13px; font-size: 13px; line-height: 1; white-space: nowrap;
}
.ly-head .btn.icon { width: 32px; padding: 0; }
/* A machine's head strip: its actions are one group, the head's button height. */
@media (min-width: 801px) and (max-width: 1699px) { .hoststrip .hs-opt { display: none; } }
.hoststrip .hs-acts { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.hoststrip .hs-acts .btn { display: inline-flex; align-items: center; height: 32px; box-sizing: border-box; padding: 0 12px; font-size: 13px; line-height: 1; white-space: nowrap; }
/* A link or a badge in a head is one unit: "its process →" never breaks after "its". */
.ly-head-l > a, .ly-head-l > .badge, .ly-head-r > a, .ly-head-l > .chip { white-space: nowrap; }
/* The Problems bar's action is a head button too. */
.pbar .btn { height: 32px; box-sizing: border-box; padding: 0 13px; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; }

/* A destructive action looks the same everywhere: a small red-edged button in
   the row's last (actions) column — never a grey "delete" link, a blue
   "retire" link, a red block under a name. */
td .btn.danger, td button.danger { white-space: nowrap; }

/*
 * A field on a data page keeps a readable width: a "watch a function by
 * name" box 1530px wide on a 2560 screen is a line nobody reads to its end.
 * The button that goes with it stays beside it. Settings rows have their own
 * column (layout.md, "Settings pages").
 */
:is(.ly-page .card):not(.setpane *) :is(input[type=text], input[type=search], input[type=email], input[type=url], input:not([type]), select) { max-width: 480px; }
:is(.ly-page .card):not(.setpane *) textarea { max-width: 960px; }

/*
 * The assistant's mark on a phone is in the top bar (.tb-ai), beside the
 * theme: in a corner, at rest or not, it sat on a table's last column, a
 * chart, a badge (measured at 390). The corner mark is a desktop's.
 */
@media (max-width: 800px) {
  .assistantbtn { display: none !important; }
}

/* ---- phase 2: frontend pages ----
 * The application pages (overview, sessions, pages, errors, api, resources,
 * campaigns, funnels, replays, security, apps, synthetic, mobile, geo) on the
 * layout system. Only additions; where an older rule is overridden it says so.
 */
/* -- frontend A: overview, pages, errors, sessions, api, resources -- */
/* Overview: the live line is a status bar, one row where it fits. */
.ov-live { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 10px 16px; }
.ov-live b { font-size: 16px; }
.ov-live-pages { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
@media (max-width: 800px) { .ov-live-pages { margin-left: 0; white-space: normal; } }
/* The vitals are tiles in a .ly-kpis row; a vital is not a .kpi, so it gets the same shrink rule. */
.ov-vitals .ly-kpis > .vital { min-width: 0; }
/* Seven breakdowns, never a hole: 1 column on a phone, 2 below 1100 (the odd
   last one spans), 4 + 3 on a desktop (the three share the row), 7 abreast
   from 1800. Rows stretch, so a short list sits in a card as tall as its row. */
.ov-dims { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: stretch; }
.ov-dims > * { min-width: 0; margin: 0 !important; }
/* A phone pairs them too (seven full-width lists were 2000px of scrolling); a label cuts at its bar. */
@media (max-width: 800px) {
  .ov-dims { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .ov-dims > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  .ov-dims > .card { padding: 12px; }
  .ov-dims .bars .b { gap: 6px; font-size: 12px; }
  .ov-dims .bars .txt { padding: 3px 7px; }
  /* The path scrolls sideways inside its card here; the fading edge says there is more. */
  .ov-page .flowbox { -webkit-mask-image: linear-gradient(to right, var(--panel) 82%, transparent);
    mask-image: linear-gradient(to right, var(--panel) 82%, transparent); }
}
@media (min-width: 801px) {
  .ov-dims { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ov-dims > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (min-width: 1100px) {
  .ov-dims { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .ov-dims > * { grid-column: span 3; }
  .ov-dims > :nth-child(n+5) { grid-column: span 4; }
  .ov-dims > :last-child:nth-child(odd) { grid-column: span 4; }
}
/* No seven-abreast row on a wide screen: a three-row list stretched to the
   height of an eight-row one was 60% empty. Four long lists, then three short. */
/* Sessions: the controls in the table card's heading, wrapping under the title on a phone. */
.ss-controls { gap: 8px 12px; justify-content: flex-end; }
.ss-controls input[type=search] { width: 220px; max-width: 100%; }
.ss-controls select { width: auto; }
@media (max-width: 800px) {
  .ss-controls { width: 100%; justify-content: flex-start; }
  .ss-controls input[type=search] { flex: 1 1 160px; width: auto; }
  /* Four columns on a phone (replay, start, path, errors): tighter cells, no idle sort arrows, so the path keeps room. */
  .ss-list th, .ss-list td { padding-left: 6px; padding-right: 6px; }
  .ss-list th:not(.sorted) .sort-arrow { display: none; }
  /* The ▶ rides in the Started cell on a phone (the Replay column is prio 2 there). */
  .ss-list .ss-play { margin-right: 6px; padding: 2px 7px; vertical-align: middle; }
}
/* A tile whose value is a path: cut at the tile's edge, the whole of it on hover. */
.kpi .value.ss-trunc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 18px; }
/* The device facts in a detail page's side column: two columns of facts, not three slivers. */
.ly-side .devices .grid.g3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* ...while the side column follows the main one (below 1800px) it is the page's width: four abreast, not 9 rows of two. */
@media (min-width: 1100px) and (max-width: 1799px) { .ly-side .devices .grid.g3 { grid-template-columns: repeat(4, minmax(0, 1fr)); } .ly-side .devices .grid.g3 > :last-child:nth-child(4n+1) { grid-column: 1 / -1; } }
/* An error group's message in the page head: allowed to wrap, never to push the actions off. */
.ly-head .err-msg { min-width: 0; overflow-wrap: anywhere; }
/* An error group's occurrences: two abreast from 1800 (a stack is ~100 mono columns). */
@media (min-width: 1800px) {
  .err-samples { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px; }
}
/* Resources: the audit with nothing found yet is its heading line, not an empty box. */
.res-opp-empty h3 { margin-bottom: 0; }
.res-opp-empty #list { display: none; }
/* -- frontend B: campaigns, funnels, replays, geo -- */
/* Funnels: the steps as columns on a desktop — a funnel reads left to right
   and a wide screen has the width — and as rows with a bar each on a phone.
   --p is the share of the sessions that entered; --fn-n the number of steps. */
.fn-steps { display: grid; gap: 10px; grid-template-columns: repeat(var(--fn-n, 4), minmax(0, 1fr)); align-items: end; }
.fn-step { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.fn-label { display: flex; gap: 6px; align-items: baseline; font-size: 13px; min-width: 0; }
.fn-num { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.fn-num b { font-size: 18px; font-weight: 650; }
.fn-num .warn { color: var(--warn); }
.fn-track { position: relative; height: var(--fn-h, 170px); border-radius: 6px; background: var(--line); overflow: hidden; margin-top: 4px; }
.fn-track i { position: absolute; left: 0; right: 0; bottom: 0; height: var(--p, 0%); background: var(--s1); border-radius: 6px 6px 0 0; }
.fn-step.leak .fn-track i { background: var(--warn); }
.fn-leak { margin-top: 12px; color: var(--fg-2); }
@media (max-width: 800px) {
  .fn-steps { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .fn-step { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 10px; }
  .fn-num { justify-content: flex-end; }
  .fn-track { grid-column: 1 / -1; height: 10px; border-radius: 5px; margin-top: 0; }
  .fn-track i { right: auto; top: 0; height: 100%; width: var(--p, 0%); border-radius: 5px; }
}
/* Campaigns: the link builder's six fields three abreast on a wide screen
   (two rows of three instead of three rows of two 1100px inputs). Overrides
   the .grid.g2 inside this one card only. */
@media (min-width: 1800px) { .cmp-utm > .grid.g2 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.cmp-untagged { border-left: 3px solid var(--warn); }
/* World map (the page, not the full-screen one): the filters are the head —
   compact selects whose "all" choice is their label — and the map stands
   beside its table from 1400px, map three fifths, table two. The map card
   stretches to the table's height and the map fills it (the svg's own
   background is the ocean, so a taller frame letterboxes in sea). */
.geo-page select.geo-f { width: auto; min-width: 0; max-width: 220px; padding: 5px 8px; font-size: 12.5px; margin: 0; }
.geo-page .geo-f-lab { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-weight: 400; white-space: nowrap; }
.geo-split { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: stretch; }
.geo-split > * { margin: 0 !important; min-width: 0; }
@media (max-width: 800px) {
  .geo-page .ly-head-r { justify-content: flex-start; margin-left: 0; }
  .geo-page select.geo-f { max-width: 100%; flex: 1 1 140px; }
  .geo-page .geo-f-lab:has(select) { flex: 1 1 100%; }
}
@media (min-width: 1400px) {
  .geo-split { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .geo-split > .geo-mapcard { display: flex; flex-direction: column; }
  .geo-split > .geo-mapcard > #map { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
  .geo-split > .geo-mapcard > #map > .geo { flex: 1 1 auto; display: flex; flex-direction: column; }
  .geo-split > .geo-mapcard > #map > .geo > svg { flex: 1 1 auto; height: auto !important; min-height: 300px; }
}
/* A third of the page is narrow for eight columns until the screen is wide. */
@media (min-width: 1400px) and (max-width: 1799px) { .geo-split .col-p3 { display: none; } }
/* Replays: the figures above the list below 1800px; beside it, two by two in
   a side column the width of a detail page's, on a wide screen. */
.rp-band { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: start; }
.rp-band > * { margin: 0 !important; min-width: 0; }
@media (min-width: 1800px) {
  .rp-band { grid-template-columns: minmax(0, 1fr) clamp(380px, 24%, 520px); }
  .rp-band > .ly-kpis { grid-column: 2; grid-row: 1; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rp-band > .ly-table { grid-column: 1; grid-row: 1; }
}
/* -- frontend C: security, apps, synthetic, mobile -- */
/* Applications: a tile's figures follow the tile's own width (it is a half,
   a third or the whole row depending on how many applications there are):
   two abreast on a narrow tile, three from 480px, all six on one line from
   900px. Overrides the fixed two columns of .appstats, on this page only. */
.ly-apps .apptile { container-type: inline-size; }
@container (min-width: 480px) { .ly-apps .appstats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container (min-width: 900px) { .ly-apps .appstats { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
/* Synthetic: the application picker in the Checks heading keeps its own width
   (a bare select in a heading otherwise takes a line of its own). */
.synlist h3 select { width: auto; max-width: 260px; }
/* Mobile app: the four (or five) breakdowns are an ordinary balanced flow
   (layout.js lyBalance) at every width. A top-aligned grid row of four at 2560
   left a 139px hole beside the eight-row Handset card. */
/* A short list of bars beside charts (.ly-fillbars): the bars spread over the
   card's height instead of a block of them at the top of an empty card. */
.card.ly-fillbars { display: flex; flex-direction: column; }
.card.ly-fillbars > :not(h3) { flex: 1 1 auto; display: flex; flex-direction: column; }
.card.ly-fillbars > :not(h3) > .bars { flex: 1 1 auto; justify-content: space-evenly; }
/* A synthetic check: the figures (two charts, the screenshots) stacked beside
   the runs on a wide screen; below 1800px the charts are the usual two-up row
   (.ly-charts.ly-max2) and the runs follow under them. */
.syn-stack, .syn-wide { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); min-width: 0; }
.syn-wide { align-items: start; }
.syn-stack > *, .syn-wide > * { min-width: 0; margin: 0 !important; }
@media (min-width: 1800px) {
  .syn-wide { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .syn-wide .syn-stack > .ly-charts.ly-max2 { grid-template-columns: minmax(0, 1fr); }
  .syn-wide .syn-stack > .ly-pair { grid-template-columns: minmax(0, 1fr); }
  /* The charts share the height of the runs beside them (their charts are fill: app.js). */
  .syn-wide { align-items: stretch; }
  .syn-wide > .syn-stack { display: flex; flex-direction: column; gap: var(--ly-gap); }
  .syn-wide .syn-stack > .ly-charts.ly-max2 { flex: 1 1 auto; grid-auto-rows: 1fr; }
}
/* Security: a long version string ("6.0.0 (addon-fit 0.11.0)") truncates on a
   phone instead of pushing the status column out of the card. */
.sec-ver { display: block; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 800px) { .sec-ver { max-width: 11ch; } }
/* The ? beside an empty state's title keeps a space from the word before it. */
.ly-apps .ly-empty b > .hintwrap, .ly-page .ly-empty b > .hintwrap { margin-left: 6px; }
/* On a phone the breakdowns and the screens keep all their figures in the
   card: narrower cell padding, and the idle sort arrows (a tap on the heading
   still sorts) give back the ~60px a 390 screen was short of. */
@media (max-width: 800px) {
  .mob-tight th, .mob-tight td { padding-left: 5px; padding-right: 5px; }
  .mob-tight th { letter-spacing: .02em; }
  .mob-tight th:not(.sorted) .sort-arrow { display: none; }
}
/* ---- end of phase 2: frontend pages ---- */

/* ---- phase 2: settings ----
 *
 * The settings blocks (layout.js settingsLayout / settingsSection /
 * settingsRow / settingsFoot / settingsNote; layout.md "Settings pages").
 * Shared by both settings developers: append inside this section, re-read it
 * first, never rewrite another's rule.
 */
/* The frame: the tab list down the left, the open tab beside it. */
.setpage > .setwrap { min-width: 0; }
.setpane.ly-page { gap: var(--ly-gap); }
@media (max-width: 800px) {
  .setwrap { display: block; }
  .setnav { flex-direction: row; flex-wrap: wrap; gap: 6px; position: static; margin-bottom: 14px; }
  .setnav-item { border-left: none; border: 1px solid var(--line); padding: 5px 10px; font-size: 13px; }
  .setnav-item.on { border-color: var(--accent); }
}
/* One column of sections; from 1800px the form keeps 1100px and the side
   column (status, summaries, secondary sections) takes the rest. */
.ly-setgrid { display: grid; gap: var(--ly-gap); grid-template-columns: minmax(0, 1fr); align-items: start; }
.ly-set-main, .ly-set-side { display: flex; flex-direction: column; gap: var(--ly-gap); min-width: 0; }
.ly-set-main > *, .ly-set-side > * { margin: 0 !important; min-width: 0; }
.ly-set-side:empty { display: none; }
@media (min-width: 1800px) {
  .ly-setgrid { grid-template-columns: minmax(0, 1100px) minmax(380px, 1fr); }
  .ly-setgrid:has(> .ly-set-side:empty) { grid-template-columns: minmax(0, 1fr); }
}
/* A section: heading, then rows divided by hairlines. */
.ly-set > h3 { margin-bottom: 4px; }
.ly-set > h3 .ly-set-t { color: var(--fg); font-size: 14px; }
.ly-set-row {
  display: grid; grid-template-columns: clamp(150px, 38%, 340px) minmax(0, 1fr);
  gap: 6px 24px; align-items: center; padding: 12px 0; border-top: 1px solid var(--line);
}
.ly-set > h3 + .ly-set-row { border-top: 0; padding-top: 8px; }
.ly-set-row.ly-set-stack { grid-template-columns: minmax(0, 1fr); align-items: start; }
.ly-set-l { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ly-set-label { font-size: 13.5px; font-weight: 600; color: var(--fg); display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ly-set-sub { font-size: 12px; color: var(--fg-3); line-height: 1.4; }
.ly-set-r { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
.ly-set-r > label, .ly-set-r label.ly-check { margin: 0; }
.ly-set-r > input:not([type=checkbox]):not([type=radio]), .ly-set-r > select { flex: 1 1 200px; max-width: 420px; width: auto; }
.ly-set-r > .tablewrap, .ly-set-r > div:only-child { flex: 1 1 100%; min-width: 0; }
.ly-set-r > .range { flex: 0 0 auto; }
.ly-set > h3 + div > .ly-set-row:first-child { border-top: 0; padding-top: 8px; }
.ly-set-r .ly-set-unit { color: var(--fg-3); font-size: 12px; }
.ly-set-row .ly-set-r > input.ly-num:not([type=checkbox]) { width: 104px; flex: 0 0 104px; }
/* A form inside a row (invite, new team): fields and the button on one line. */
.ly-set-inline { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; flex: 1 1 100%; min-width: 0; margin: 0; }
.ly-set-inline > input:not([type=checkbox]), .ly-set-inline > select { flex: 1 1 180px; width: auto; min-width: 0; }
.ly-set-inline > select { flex: 0 1 230px; }
.ly-set > h3 + div > .tablewrap, .ly-set > h3 + div > div > .tablewrap { margin-top: 4px; }
/* One button size on a settings page: the normal one, the height of a field.
   Buttons inside a table row stay small (.btn.sm), as everywhere. */
.ly-set-r > .btn, .ly-set-foot .btn, .ly-set-inline > .btn, .ly-set-r > a.btn { padding: 7px 13px; font-size: 13px; line-height: 1.25; white-space: nowrap; }
/* The foot: the actions under the controls they act on. */
.ly-set-foot { display: grid; grid-template-columns: clamp(150px, 38%, 340px) minmax(0, 1fr); gap: 24px;
  padding-top: 14px; border-top: 1px solid var(--line); }
.ly-set-act { grid-column: 2; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
/* A line that must be read: deletes, cannot be undone, leaves the install. */
.ly-set-note { margin: 10px 0 0; padding: 8px 12px; border-radius: 7px; font-size: 12.5px; line-height: 1.45;
  color: var(--fg-2); background: var(--panel-2); border-left: 3px solid var(--warn); max-width: none; }
.ly-set-note-bad { border-left-color: var(--bad); }
.ly-set-note-info { border-left-color: var(--accent); }
.ly-set-row .ly-set-note { grid-column: 1 / -1; margin: 0; }
/* An on/off setting: a native checkbox drawn as a switch (still a checkbox to
   keyboards, screen readers and the checks that tick it). */
input[type=checkbox].ly-sw {
  appearance: none; -webkit-appearance: none; width: 34px; height: 19px; flex: 0 0 34px; margin: 0; padding: 0;
  border: 1px solid var(--line-2); border-radius: 999px; background: var(--panel-2); cursor: pointer;
  position: relative; transition: background .15s, border-color .15s; vertical-align: middle;
}
input[type=checkbox].ly-sw::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%;
  background: var(--fg-3); transition: transform .15s, background .15s;
}
input[type=checkbox].ly-sw:checked { background: color-mix(in srgb, var(--accent) 24%, transparent); border-color: var(--accent); }
input[type=checkbox].ly-sw:checked::after { transform: translateX(15px); background: var(--accent); }
input[type=checkbox].ly-sw:disabled { opacity: .5; cursor: default; }
input[type=checkbox].ly-sw:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
label.ly-check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg-2); font-weight: 500; cursor: pointer; }
/* A grid of boxes (which pages redraw): columns as the width allows. */
.ly-set-boxes { display: grid; gap: 2px 18px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr)); flex: 1 1 100%; }
.ly-set-boxes > label, .ly-set-boxes > label.instrow { display: flex; align-items: flex-start; gap: 8px; margin: 0; padding: 5px 0;
  border: 0; font-size: 13px; color: var(--fg); font-weight: 500; cursor: pointer; min-width: 0; background: none; }
.ly-set-boxes > label > input { margin-top: 2px; flex: none; width: auto; }
.ly-set-boxes > label > span { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ly-set-boxes > label b { font-weight: 550; }
.ly-set-boxes .instwhere { font-size: 11.5px; color: var(--fg-3); font-weight: 400; line-height: 1.35; }
/* Facts in a side card: key left, value right, one per line. */
.facts.ly-set-facts { margin-top: 8px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr)); }
.facts.ly-set-facts > .ly-f-wide { grid-column: 1 / -1; }
/* A share of the whole, as a bar, in a row's sub line (what uses the database). */
.ly-set-share { display: block; height: 4px; border-radius: 2px; background: var(--panel-2); overflow: hidden; margin-top: 4px; max-width: 220px; }
.ly-set-share > i { display: block; height: 100%; background: var(--accent); }
/* Badges never break letter by letter in a narrow cell. */
.setpane .badge { white-space: nowrap; }
@media (max-width: 800px) {
  .ly-set-row, .ly-set-foot { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  .ly-set-act { grid-column: 1; }
  .ly-set-r > input:not([type=checkbox]):not([type=radio]), .ly-set-r > select { max-width: none; }
}
/* -- settings B: agent & keys, health & overhead, notifications, AI, app settings, host settings -- */
/* Health & overhead: a card per machine in a grid. Overrides the older
   `.ahost` (a block inside one card: top border, no padding on the first). */
.ahgrid > .card.ahost, .ahgrid > .card.ahost:first-child { border-top: 1px solid var(--line); padding: 16px; display: flex; flex-direction: column; }
.ahgrid > .card.ahost.ahost-bad { box-shadow: inset 3px 0 0 var(--bad), var(--shadow); }
.ahgrid .ahost h3 { margin-bottom: 6px; }
.ahgrid .ahrows { margin-top: 6px; }
.ahoff { margin-top: 10px; line-height: 1.5; }
.ahoff span[title] { border-bottom: 1px dotted var(--line-2); cursor: help; }
.ahctlrow { margin-top: auto; padding-top: 12px; display: flex; }
/* An empty list inside a section: one quiet line, not a centred box. */
.ly-set .setb-empty { margin: 6px 0 4px; }
/* AI: a model is a bordered block of rows inside the Models section. */
.setb-model { border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px 12px; margin: 8px 0 12px; }
.setb-model-h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding-bottom: 6px; }
.setb-model-h .spacer { flex: 1; }
.setb-model .ly-set-row:first-of-type { border-top: 0; }
.setb-model .airoles { margin: 0; }
.ly-set-row .ly-set-r > input.setb-wide-num:not([type=checkbox]) { flex: 0 1 220px; }
.ly-set-r label.ly-check input.ly-num { width: 104px; }
/* The policy preview: severity, kind, machine and Try on one line. */
.setb-preview > select { flex: 0 1 140px; }
.setb-preview > select[name=kind] { flex: 1 1 200px; }
/* Application settings: one form of several sections, and a save bar that
   stays in view at the bottom of the screen while the form is longer. */
.setb-appform { display: flex; flex-direction: column; gap: var(--ly-gap); min-width: 0; margin: 0; }
.setb-appform > * { margin: 0 !important; }
.setb-savebar { position: sticky; bottom: 10px; z-index: 5; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 16px; background: var(--panel); border: 1px solid var(--line-2); border-radius: var(--radius); box-shadow: var(--shadow); }
.setb-savebar .btn { padding: 7px 18px; }
.setb-savebar .warn { color: var(--warn); }
.setb-slow { flex: 1 1 100%; min-width: 0; }
.setb-slow .slowrow > label { margin: 0; }
.ly-set-r > .attrs { flex: 1 1 100%; min-width: 0; }
/* The agent's Stop/Start (agentStateControl, shared, a small button) in a
   settings row takes the row's one button size. */
.ly-set-r .agentctl .btn.sm { padding: 7px 13px; font-size: 13px; line-height: 1.25; }
/* The root-terminal choice (terminalChoice, shared) inside a warning note: it
   is the one line on Agent & keys that must be read before copying. */
.ly-set-note.setb-term { margin: 0; flex: 1 1 100%; }
.setb-term > label { margin: 0 !important; flex-direction: row !important; flex-wrap: nowrap !important; }
.setb-blind { margin: 0; padding-left: 18px; line-height: 1.5; }
.setb-blind > li { margin-bottom: 4px; }
/* -- end of settings B -- */
/* The figures at the top of a settings tab: three by two on a laptop, six
   abreast on a wide screen, pairs on a phone — never five and a lonely one. */
@media (min-width: 801px) { .ly-kpis.ly-set-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1800px) { .ly-kpis.ly-set-kpis { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (min-width: 1800px) { .ly-kpis.ly-set-kpis:has(> :nth-child(5):last-child) { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
/* Five on a laptop: three, then two halves — not three and a hole. */
@media (min-width: 801px) and (max-width: 1799px) {
  .ly-kpis.ly-set-kpis:has(> :nth-child(5):last-child) { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .ly-kpis.ly-set-kpis:has(> :nth-child(5):last-child) > * { grid-column: span 2; }
  .ly-kpis.ly-set-kpis:has(> :nth-child(5):last-child) > :nth-child(n+4) { grid-column: span 3; }
}
@media (min-width: 801px) and (max-width: 1799px) { .ly-kpis.ly-set-kpis:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ly-set-kpis .kpi .label { display: flex; align-items: center; gap: 6px; }
/* Boxes without a second line pack tighter: two abreast even on a phone. */
.ly-set-boxes.ly-set-boxes-sm { grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr)); }
/* On a phone a switch or a badge stays beside its label: a row of its own for
   one small mark is a screenful of scrolling. */
@media (max-width: 800px) {
  .ly-set-row:has(> .ly-set-r > input.ly-sw:only-child), .ly-set-row:has(> .ly-set-r > .badge:only-child) {
    grid-template-columns: minmax(0, 1fr) auto; }
}
/* Two halves of one answer inside a section (by person / by thing): side by
   side from 1100px, each with its own small label. */
.ly-set-two { display: grid; gap: 10px 18px; grid-template-columns: minmax(0, 1fr); padding-top: 8px; }
.ly-set-two > * { min-width: 0; }
.ly-set-two .ly-set-label { margin-bottom: 4px; }
@media (min-width: 1100px) { .ly-set-two { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* A <form> wrapping rows (Google's keys): the first row loses its hairline too. */
.ly-set > h3 + div > form > .ly-set-row:first-child { border-top: 0; padding-top: 8px; }
.ly-set-r > input[readonly] { max-width: none; }
/* A stacked table on a settings tab (a phone): the first cell is the card's
   title — the account, the address — read as one line that wraps, badges after
   it; the rest are label + value with the values on one vertical line, not
   pushed apart (a figure and its word "in" ended up at opposite edges). */
@media (max-width: 720px) {
  .setpane .tablewrap.stacked td { justify-content: flex-start; flex-wrap: wrap; gap: 4px 10px; }
  .setpane .tablewrap.stacked td::before { flex: 0 0 34%; }
  .setpane .tablewrap.stacked td > .tdv { flex: 1 1 0; text-align: left; }
  .setpane .tablewrap.stacked td:first-child { display: block; padding-top: 9px; }
  .setpane .tablewrap.stacked td:first-child::before { content: none; }
}
/* Nothing in a section yet: one line on what will be here, centred. */
.ly-set-empty { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 28px 12px; text-align: center; }
.ly-set-empty b { font-size: 14px; color: var(--fg); }
/* A phone: the section's name, its ? and its count on the first line, the
   grey line under them — not the count alone on a line of its own. */
@media (max-width: 800px) {
  .ly-set > h3 .drill { order: 9; flex-basis: 100%; }
}
/* -- settings B, 2026-10-28 review -- */
/* A form saved by its button says so while it holds an edit (watchUnsaved). */
.ly-set-dirty { color: var(--warn); font-weight: 550; }
/* "all · none" in a row's grey line: links, the size of the line. */
.ly-set-sub a[data-allpages] { color: var(--accent); }
/* A section's own status in its heading (Google: offered / off). */
.ly-set > h3 #oauthstate:empty { display: none; }
/* The cost line on a machine's card: the count of mechanisms not in use and its `?` on one line. */
.ahoff { display: flex; align-items: center; gap: 6px; }
/* A short command inside a must-read note ("nginx -t") is never broken at its hyphen.
   Only one marked so: a long header line in a note must still wrap on a phone. */
.ly-set-note .mono.ly-set-cmd { white-space: nowrap; }
/* Health & overhead, round 2: the machines packed in a flow (each card its own height, no stretch). */
.ahflow > .card.ahost, .ahflow > .card.ahost:first-child { border-top: 1px solid var(--line); padding: 16px; }
.ahflow > .card.ahost.ahost-bad { box-shadow: inset 3px 0 0 var(--bad), var(--shadow); }
.ahflow .ahost h3 { margin-bottom: 6px; }
.ahflow .ahrows { margin-top: 6px; }
.ahflow .ahctlrow { margin-top: 12px; display: flex; }
/* A section that is only its heading line (What this cannot see: the list is behind its `?`). */
.ly-set.setb-oneline > h3 { margin-bottom: 0; }
/* A row with nothing to act on (your own account) draws no empty action row on a phone card. */
@media (max-width: 720px) {
  .setpane .tablewrap.stacked td:empty, .setpane .tablewrap.stacked td:has(> .tdv:empty) { display: none; }
}
/* A cost said in a switch's label ("~5% of a core"): quieter than the name. */
.ly-set-label .ly-set-cost { font-weight: 500; color: var(--fg-3); font-size: 12px; }
/* The AI consent text: in full, but quiet — the size of a row's grey line, a hairline at its side, one measure wide. */
.ly-set-note.ly-set-consent { background: none; border-left: 2px solid var(--line-2); padding: 0 0 0 12px; margin: 0 0 12px;
  font-size: 12px; line-height: 1.5; color: var(--fg-3); max-width: 78ch; }
/* Pause all sending, Block, a new model: the duration / provider select is short, the text field takes the rest. */
form[data-mform].ly-set-inline > select, form[data-blockform].ly-set-inline > select,
form[data-aiaddform].ly-set-inline > select { flex: 0 0 150px; }
/* A row's actions stay on one line (Password… and Remove were stacked in a narrow column). */
.ly-set-acts { white-space: nowrap; display: inline-flex; gap: 6px; }
/* Application settings' save bar (round 3): the grid's last child, and its button on the control column,
   where every other Save on a settings page stands (settingsFoot): the status where the labels are. */
.ly-setgrid > .setb-savebar { display: grid; grid-template-columns: clamp(150px, 38%, 340px) minmax(0, 1fr); gap: 24px; align-items: center; }
.setb-savebar .setb-savebar-act { display: flex; gap: 8px; align-items: center; }
@media (max-width: 800px) { .ly-setgrid > .setb-savebar { grid-template-columns: minmax(0, 1fr) auto; gap: 12px; } }
/* The figures on top of a settings tab fill the pane on a wide screen whatever their count
   (four tiles ended at 1068 px of a 2324 px pane: round 3). */
@media (min-width: 1800px) {
  .ly-kpis.ly-set-kpis:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .ly-kpis.ly-set-kpis:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Round 4 (2026-10-29 review). A command breaks at its spaces, never inside the URL; only a token longer than
   the line may break anywhere. */
.setpane .snippet { white-space: pre-wrap; word-break: normal; overflow-wrap: anywhere; }
/* A segmented choice in a row is as wide as its options (Personal: "Redraw every" was stretched to 540 px). */
.ly-set-r > div.range, .ly-set-r > div.seg, .ly-set-r > .range:only-child { flex: 0 0 auto !important; width: auto; display: inline-flex; }
/* A name's badges stay together after it, as one group. */
.ly-badges { white-space: nowrap; }
/* Controls inside a settings table row are one height: a button, a select, a field (Notifications had 15/30/36/30). */
.setpane td select, .setpane td input:not([type=checkbox]):not([type=radio]) { height: var(--field-h, 32px); padding-top: 0; padding-bottom: 0; font-size: 12.5px; }
.setpane td .btn.sm { min-height: var(--field-h, 32px); display: inline-flex; align-items: center; }
/* The agent's state beside its Stop/Start: the pill as tall as the button. */
.setpane .agentctl .badge { min-height: 28px; display: inline-flex; align-items: center; }
/* A must-read note takes the section's width, not the 90ch of running text, so the right edge is not ragged. */
.ly-set .ly-set-note { max-width: none; }
/* Slowness (round 5): the band in the sliders' column, inset by half a thumb, so a colour ends under its thumb. */
.slowrow.slowbarrow { margin-bottom: 4px; }
.slowbarrow > .slowbar { margin: 0 8px; }
/* One field height on every settings page and in every dialog (round 5: 36 px here, 34 in a dialog,
   32 inline): a field, a select and a row's button stand 32 px tall. Textareas and snippets keep their own. */
.setpane, .modal, .setb-app { --field-h: 32px; }
.setpane input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]), .setpane select,
.setb-app input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]), .setb-app select {
  height: var(--field-h); box-sizing: border-box; padding-top: 0; padding-bottom: 0; }
.ly-set-r > .btn, .ly-set-foot .btn, .ly-set-inline > .btn, .ly-set-r > a.btn { min-height: var(--field-h, 32px); }
/* A one-line form's text field (one-time link, invite, new key) stops at 480 px: at 2560 it ran 1500 px wide (round 5). */
.ly-set-inline > input:not([type=checkbox]) { max-width: 480px; }
/* A long address (the Google redirect) breaks where it must instead of pushing its Copy off the row. */
.setb-uri { overflow-wrap: anywhere; word-break: break-all; }
/* A `?` never sticks to the word before it ("window.?", "service(?)"); the places with their own gap keep it. */
.hintwrap { margin-left: 4px; }
/* Host settings → Agent: no rows above it, so Update now starts at the card's left edge (round 5: it sat in the empty control column). */
.ly-set-foot.setb-agentfoot { display: block; }
/* Alert email per account: the threshold select as wide as its longest choice ("default · critical" was cut to 118 px at 1440, review 5 #5). */
select.setb-amin { min-width: 158px; width: auto; }
/* The model being added: a red line under a refused key stays one line. */
.setb-newmodel [data-nmkey] .bad { overflow-wrap: anywhere; color: var(--bad); }
.setb-newmodel [data-nmkey] .good { color: var(--good); }
/* A confirmation: one measure wide, its text in full. */
.modal.modal-confirm { max-width: 520px; }
/* An edited dialog's "discard what you typed?": over its footer, inside the dialog. */
.modal-discard { position: sticky; bottom: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 12px -4px 0; padding: 10px 12px;
  border-radius: 8px; background: var(--panel-2); border: 1px solid var(--warn); font-size: 13px; z-index: 2; }
.modal-discard > span { flex: 1 1 200px; }
/* The AI consent once given: the same text one click under its switch. */
.ly-set-consent-d { margin: 0 0 12px; }
.ly-set-consent-d > summary { font-size: 12.5px; color: var(--accent); cursor: pointer; width: max-content; }
.ly-set-consent-d > .ly-set-consent { margin: 8px 0 0; }
/* A model: one summary line under its name; the form behind Edit. */
.setb-model-s { margin: 0 0 2px 16px; }
.setb-model-b > .ly-set-row:first-child { margin-top: 8px; }
/* A model's "In use" box beside its name, on one line. */
.setb-model-h label.ly-check { white-space: nowrap; }
/* The invitation: address, role and button on one line in a laptop's form column. */
form[data-invite].ly-set-inline > select { flex: 0 0 120px; }
/* An on/off switch in a table cell (AI per organisation): no label box around it. */
td label.row > input.ly-sw { margin: 0; }
/* Who sees what: by person and by thing abreast only on a wide screen (at 1440 two halves of
   ~450px broke every address in two), and never when "by thing" is one line (no exceptions). */
@media (max-width: 1799px) { .ly-set-two { grid-template-columns: minmax(0, 1fr); } }
.ly-set-two.ly-set-one { grid-template-columns: minmax(0, 1fr); }
/* -- end of settings B, 2026-10-28 review -- */
/* ---- end of phase 2: settings ---- */

/* ---- phase 2: server data pages ----
 * Processes, traces, log patterns, databases, containers, profiling, code and
 * server security on the layout system (layout.md). Only what the blocks do
 * not already give; every rule is scoped to these pages' own classes or to
 * .ly-page, and overrides an older rule only where it says so.
 */

/* Trace page: the context of the call (browser side, logs, agent, headers)
   is its own card beside the waterfall; its first heading needs no gap. */
.tctxcard .tctx > :first-child { margin-top: 0 !important; }
/* The waterfall's tick lines are 1000px tall by design (they run under every
   row); clipped at the waterfall, they no longer run on below the card. */
.ly-page .twf { overflow: hidden; }

/* Log patterns: one compact line per pattern (overrides the 230px action
   column and the stacked meta of the older rule), two columns of them on a
   wide screen. A row past the first screenful is [hidden] until "show all"
   (the global [hidden] rule keeps it hidden against the grid above). */
.ly-page .lprow { grid-template-columns: 22px minmax(0, 1fr) auto; align-items: center; gap: 6px 14px; padding: 9px 0; }
.ly-page .lpmeta { margin-top: 4px; gap: 4px 10px; }
.ly-page .lpbarv { display: inline-block; width: 84px; margin: 0; vertical-align: middle; }
.ly-page .lpsample { margin: 0; }
.ly-page .lpmeta > details[open] { flex-basis: 100%; }
.ly-page .lpact { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px 10px; }
.ly-page .lpverdict { margin-top: 0; }
.ly-page .lpwhy { max-width: 220px; text-align: right; }
.lpusual { margin: -4px 0 4px; }
.lpsaved-h { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; }
.lpsaved-h .lpnum-c { width: 22px; height: 22px; font-size: 12px; }
/* Its own heading row (the stat cells around it are columns of a big number over a label). */
.lpstats > .lpsaved-h { flex-direction: row; }
.lpstats > .lpsaved-h b { font-size: 13px; font-weight: 650; }
@media (min-width: 1800px) {
  .ly-page .lplist { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 36px; }
}
@media (max-width: 900px) {
  .ly-page .lprow { grid-template-columns: 22px minmax(0, 1fr); }
  .ly-page .lpact { grid-column: 2; justify-content: flex-start; }
  .ly-page .lpwhy { max-width: none; text-align: left; }
}

/* Code: what the agent instruments, as a grid of switches rather than one
   2300px row each: two abreast on a desktop, four on a wide screen (seven
   libraries read 4 + 3, where three columns left a lone one). */
.p2-instgrid { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: 32px; }
.p2-instgrid > .instrow { border-top: 0; border-bottom: 1px solid var(--line); padding: 9px 4px; }
@media (min-width: 1100px) { .p2-instgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1800px) { .p2-instgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Server security. The tiles share the row they are in (auto-fit, where the
   older rule's auto-fill left empty tracks to the right of four machines). */
.ly-page .stiles { grid-template-columns: repeat(auto-fit, minmax(min(100%, 270px), 1fr)); margin-bottom: 0; }
.p2-seccol { display: flex; flex-direction: column; min-width: 0; }
.p2-seccol > .sechd { margin: 0 0 10px; }
.p2-seccol > .stiles, .p2-seccol > .atkover { flex: 1 1 auto; margin-bottom: 0; }
.p2-secrow { display: flex; flex-direction: column; gap: var(--ly-gap); }
@media (min-width: 1800px) {
  /* Applications and the doors on one row, each spanning as many tracks as it has tiles. */
  .p2-secrow { display: grid; grid-template-columns: repeat(var(--p2n, 2), minmax(0, 1fr)); align-items: stretch; }
  .p2-secrow > :first-child { grid-column: span var(--p2a, 1); }
  .p2-secrow > :last-child { grid-column: span var(--p2b, 1); }
}
/* Doors without the attack map: the door tiles share the row themselves
   (the older grid kept a 2.2fr track for a map that is not there). */
.ly-page .atkover:not(:has(.atkmaptile)) { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); margin-bottom: 0; }
.ly-page .atkover:not(:has(.atkmaptile)) > .atkstack { display: contents; }
/* Traces on a phone: the day and the machine step back, so the status and
   the time of every call stay on the screen (the status was cut off). */
@media (max-width: 800px) {
  .p2-calls .p2-day, .p2-calls .p2-host { display: none; }
  .p2-calls td, .p2-calls th { padding-left: 6px; padding-right: 6px; }
  .p2-calls td b { display: inline-block; max-width: 84px; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
  /* The headers still sort on a tap; the arrows cost a column's width. */
  .p2-calls th .sort-arrow { display: none; }
}
/* The waterfall on a phone: two levels per row (UI review 4 #1) — the name
   across the whole width, the bar and its time under it. Side by side the
   name had ~150 px after its badges ("POS…", "chec"). The name shares its
   line with the duration; the bar runs the full width under it, as the ruler
   does. The technology tag keeps its icon, the source pill and the
   start → end figures go (the bar says where it sits); only the ends of the
   ruler are labelled. */
@media (max-width: 800px) {
  .ly-page .twfrow { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: 'n m' 'b b'; gap: 2px 8px; }
  .ly-page .twfrow > .twfname { grid-area: n; }
  .ly-page .twfrow > .twfbar { grid-area: b; }
  .ly-page .twfrow > .twfms { grid-area: m; }
  .ly-page .twfruler, .ly-page .twfaxis { grid-template-areas: 'b b'; }
  .ly-page .twfruler > .twfname, .ly-page .twfaxis > .twfname,
  .ly-page .twfruler > .twfms, .ly-page .twfaxis > .twfms { display: none; }
  .ly-page .twf .twfrow:not(.twfruler):not(.twfaxis) { padding-top: 3px; padding-bottom: 3px; }
  .ly-page .twfname .ktag .ktw, .ly-page .twfname .src { display: none; }
  .ly-page .twfms .twfat { display: none; }
  .ly-page .twfms { white-space: nowrap; }
  .ly-page .twfaxis .twftlabel:not(:first-of-type):not(:last-of-type) { display: none; }
}
/* ---- end of phase 2: server data pages ---- */

/* ---- phase 2: dashboards ----
 * IoT, technologies, SLOs, the dashboard, trends, business logic, browser,
 * data store, the console's machine list, transfers, licences and API keys on
 * the layout system (layout.md). Tokens only; nothing above is overridden
 * except where a page moved onto the system and an older cap no longer fits.
 */

/* IoT: a card per device in the chart grid. The list's old half-width cap
   was for one long card; a device card is already at most half a row. */
.ly-page .iotlist { max-width: none; }
.iotdev { display: flex; flex-direction: column; container-type: inline-size; }
.iotdev > .iotrow { flex: 1 1 auto; padding: 0; border-top: 0; }
/* A quarter of a wide screen: the facts go above the probes once the card is narrow. */
@container (max-width: 500px) { .iotdev > .iotrow { grid-template-columns: minmax(0, 1fr); } }
.iotboard .facts { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); }

/* Technologies: one grid of tiles across the page, the group as a kicker. */
.techgrid.techall { grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); }
.techtop { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
.techgroup { font-size: 10.5px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; color: var(--fg-3); }
.techdetail .pinrow { grid-template-columns: minmax(0, 1fr) auto; }
/* Where it was found: a tile per machine, process or service, as many to a row as fit. */
.techrows { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.techrows > .pinrow, .techrows > .pinrow:first-child { padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); }

/* SLOs: the four figures of an objective share its card's width. */
.slocard { display: flex; flex-direction: column; }
.slonums { display: grid; gap: 12px 20px; align-items: start; margin-bottom: 14px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 130px), 1fr)); }
.slofig { font-size: 26px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.2; }
.slobar { margin-top: auto; height: 8px; border-radius: 4px; background: var(--line); overflow: hidden; }
.slobar > i { display: block; height: 100%; }

/* Trends: the difference from usual inside the day's card, on the same clock. */
/* The older .trends grid asked its cells to align at the start; a page stretches its blocks. */
.ly-page.trends { align-items: stretch; }
.ly-page.trends.busy .card:not(.trendctl) { opacity: .45; transition: opacity .2s; }
/* On a wide screen the pickers share the controls card instead of stopping at 340px each. */
@media (min-width: 1800px) {
  .ly-page.trends .trendpick .f { max-width: none; }
  .ly-page.trends .trendctl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.tr-diff { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
.tr-sub { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin: 0 0 6px; font-size: 12px; font-weight: 650; }

/* Business logic and the data store. */
.bizempty { margin: 0; }
/* A card with five fields fills its row rather than leaving two empty tracks after them. */
/* A wall of equal tiles (layout.js tileGrid): the columns that fit, or one
   fewer when that leaves fewer gaps in the last row; every tile one width. */
.ly-tiles { --ly-tcols: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); }
.ly-tiles.ly-tiles[style*="--ly-tcols"] { grid-template-columns: repeat(var(--ly-tcols), minmax(0, 1fr)); }
.ly-tiles > * { min-width: 0; margin: 0; }
.storeq { flex: 1 1 240px; min-width: min(100%, 240px); max-width: 420px; }
/* Beside a long list the globe sits in the middle of its card, not at the top of an empty one. */
.ly-pair.storegrid > .cloudcard { display: flex; flex-direction: column; }
.ly-pair.storegrid > .cloudcard > #cloud { flex: 1 1 auto; display: flex; flex-direction: column; }
/* The globe takes the card's height under its line of text (beside a long list), not a fixed box centred in it. */
.ly-pair.storegrid > .cloudcard > #cloud > .cloud { flex: 1 1 auto; height: auto; min-height: clamp(320px, 46vh, 560px); }
.ly-pair.storegrid > .cloudcard > #cloud > .cloud > .cloudsvg { position: absolute; inset: 0; } /* never props its box up: the list beside decides the height */

/* Console (a special page): the machines as tiles above the terminal. */
.congrid { display: grid; gap: 10px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1100px) { .congrid { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); } }
.conhost { min-width: 0; padding: 2px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); }
.conhost > .conrow:first-child { border-top: 0; padding-top: 9px; }

/* Transfers: three hand-overs side by side, the picker at the foot of each. */
.xfer { display: flex; flex-direction: column; }
.xfer > p { margin: 0 0 12px; }
.xferpick { gap: 10px; align-items: center; margin-top: auto; }

/* Licences: the page is the system's; the pricing card brings its own margin. */
.licpage [data-licpricing]:empty { display: none; }
.licpage [data-licpricing] > .card { margin: 0 !important; }
.licbar { gap: 8px; align-items: center; margin-bottom: 10px; }
.licbar input[type=search] { flex: 1 1 220px; max-width: 320px; min-width: 0; }
/* ---- end of phase 2: dashboards ---- */

/* ---- round 4 (UI review 3), agent A: data pages ---- */
/* A database's two small charts: side by side, one under the other on a phone
   (two 160px halves drew 2–4px axis labels). */
.dbmini { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
@media (max-width: 800px) { .dbmini { grid-template-columns: minmax(0, 1fr); } }
/* A waterfall row's name: the label itself gives way with an ellipsis, the
   badges after it stay whole ("pg SELECT … FOR UP" was cut mid-word, no ellipsis,
   because the badges were the end of the line). */
.twfname { display: flex; align-items: center; gap: 4px; min-width: 0; }
.twfname > * { flex: none; }
.twfname > .twflbl { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A request's call chain of two or three services: one line, not a map. */
.tchain { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 18px; font-size: 13px; }
.tchain > b { color: var(--fg-2); font-weight: 650; margin-right: 4px; }
.tchain-n { display: inline-flex; align-items: baseline; gap: 6px; padding: 4px 10px; border: 1px solid var(--line-2); border-radius: 7px; background: var(--panel-2); }
.tchain-n.is-current { border-color: var(--accent); }
.tchain-n.bad { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }
.tchain-a { color: var(--fg-3); }
/* A flow path on a phone (charts.js drawFlowList): the stages one under the other. */
.flowwrap.is-list .flowbox { overflow: visible; -webkit-mask-image: none; mask-image: none; }
.flist { display: flex; flex-direction: column; gap: 6px; }
.flist-st { display: flex; flex-direction: column; gap: 4px; }
.flist-cap { font-size: 10.5px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); }
.flist-n { display: flex; align-items: baseline; gap: 8px; padding: 7px 10px; border: 1px solid var(--line-2); border-radius: 8px;
  background: var(--panel-2); color: var(--fg); min-width: 0; text-decoration: none; }
.flist-n.is-current { border-color: var(--accent); }
.flist-n.h-bad { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }
.flist-n.h-warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }
.flist-l { font-weight: 600; font-size: 12.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; }
.flist-s { font-size: 11px; color: var(--fg-3); margin-left: auto; white-space: nowrap; flex: none; }
.flist-more { font-size: 11.5px; color: var(--fg-3); padding-left: 10px; }
.flist-arrow { color: var(--fg-3); text-align: center; line-height: 1; }
.flow-dot-i { width: 8px; height: 8px; border-radius: 50%; background: var(--fg-3); flex: none; align-self: center; }
.flow-dot-i.t-visit { background: var(--c-app); }
.flow-dot-i.t-own { background: var(--c-domain); }
.flow-dot-i.t-service { background: var(--c-service); }
.flow-dot-i.t-data { background: var(--s3); }
.flow-dot-i.t-host { background: var(--c-host); }
/* A segmented control on a phone wraps to a second row instead of hiding its
   last choices behind an invisible sideways scroll ("Always stored", "All" on
   Log patterns). Each choice keeps its own edges, so a second row reads. */
@media (max-width: 800px) {
  .seg:is([role=group], [role=tablist]) { flex-wrap: wrap; overflow: visible; border: 0; gap: 4px; }
  .seg:is([role=group], [role=tablist]) a, .seg:is([role=group], [role=tablist]) button {
    flex: 1 1 auto; border: 1px solid var(--line) !important; border-radius: 7px; text-align: center; }
}
/* A session's device facts on a phone: two abreast, not sixteen rows of one (1299px, 38% of the page). */
@media (max-width: 800px) { .ly-page .ly-side .devices .grid.g3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* A disabled button looks disabled — everywhere, danger included ("Stop instrumenting" was full red while it did nothing). */
.btn:disabled, .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; filter: saturate(.4); }
.btn:disabled:hover { background: var(--panel); border-color: var(--line-2); }
.btn.primary:disabled:hover { background: var(--accent); border-color: var(--accent); }
/* Server security's status line is its page head now (not a footer). */
.ly-head.secfoot { padding: 0; border-top: 0; }
/* Every problem category's table: the same column lines, card to card (app.js problemTable). */
@media (min-width: 801px) {
  .ptab table { table-layout: fixed; }
  .ptab th:nth-child(1) { width: 104px; }
  .ptab th:nth-child(3) { width: 18%; }
  .ptab th:nth-child(4) { width: 132px; }
  .ptab th:nth-child(5) { width: 104px; }
  .ptab th:nth-child(6) { width: 84px; }
}
/* A log line reads at normal weight; only a structured line's message is set off (.logk). */
.ly-page .logmsg { font-weight: 400; }
/* A session's start: the ▶ (when there is a recording) before the time, aligned on one line of slots. */
.ss-start { display: flex; align-items: center; gap: 8px; }
.ss-start > .ss-play { margin: 0; padding: 2px 8px; flex: none; }
.ss-start > .ss-noplay { width: 30px; flex: none; }
@media (max-width: 800px) { .ss-start > .ss-noplay { display: none; } }
/* On a wide screen the entry and the exit stand on one line, side by side: two
   lines of short paths in a 1360px column used a fifth of it. */
@media (min-width: 1800px) {
  .ss-list td.ly-fill > .pth { display: inline-flex; vertical-align: middle; max-width: 48%; }
  .ss-list td.ly-fill > .pth + .pth { margin-left: 18px; }
}
/* The application filter in the Checks heading: the height of the button beside it. */
.synlist h3 select { height: 28px; padding: 2px 8px; font-size: 12.5px; }
/* Trend analysis on a phone: the option rows fold behind "Options" (app.js), and every control is one height. */
.trendctl .trendmore { display: none; }
@media (max-width: 800px) {
  .trendctl .trendmore { display: inline-flex; justify-self: start; align-self: flex-start; }
  .trendctl:not(.open) > .trendrow:not(:first-child) { display: none; }
  .trendctl select, .trendctl input[type=date], .trendctl .trendday .btn { height: 34px; box-sizing: border-box; }
  .trendctl .trendday .btn { display: inline-flex; align-items: center; }
}
/* The API catalogue: two columns now, and what an endpoint answers in two lines at most. */
@media (min-width: 801px) { .apicat th:nth-child(1) { width: 66%; } .apicat th:nth-child(2) { width: auto; } }
.apicat .apiabout { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
/* A destructive action in a card's heading is quiet until it is pointed at: three red
   "Delete" boxes in the heads of three objectives outshouted the figures. In a table's
   actions column it stays the plain red-edged button. */
.btn.danger.quiet { color: var(--fg-2); border-color: var(--line-2); background: transparent; } /* outlined, so it reads as a button and not a grey link (UI review 4 #36) */
.btn.danger.quiet:hover, .btn.danger.quiet:focus-visible { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); background: var(--bad-bg); }
/* A ? after words always keeps a space from them ("Parameters?" read as a question). */
:is(h3, h4, label, b, .label, .ly-set-label, .sechd h2) > .hintwrap { margin-left: 5px; }
/* The bar is one height on every page, with or without a time range in it (54 vs 61px made the title jump between pages). */
@media (min-width: 801px) { .topbar { min-height: 61px; box-sizing: border-box; } }
/* A filling column given back on a wide screen (app.js table(): its content is short):
   the columns share the row by what they hold. */
@media (min-width: 1800px) {
  .ly-page table.ly-unfill td.ly-fill { width: auto; max-width: none; }
  .ly-page table.ly-unfill td.ly-fill .truncate { max-width: none; }
}
/* A stack beside a taller card: its box takes the card's height (no empty band under it). */
.card.ly-fillpre { display: flex; flex-direction: column; }
.card.ly-fillpre > pre { flex: 1 1 auto; }
/* A rename or a short value list edited in place (inlineEdit, app.js): field, Save, Cancel on one line; the note under them. */
.inlform { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; max-width: 100%; }
.inlform input { height: 32px; width: 260px; max-width: 100%; min-width: 0; }
.inlform.is-wide { display: flex; margin-top: 8px; }
.inlform.is-wide input { flex: 1 1 260px; width: auto; max-width: 480px; }
.inlform .inlnote { flex-basis: 100%; }
.inlform:not(.is-wide) { flex-wrap: nowrap; }
.inlform:not(.is-wide) input { flex: 1 1 120px; width: 260px; }
/* One control height in an inline editor too (round 5: field 32, its buttons 28, a machine's rename 24). */
.inlform .btn { height: var(--field-h, 32px); min-height: 0; padding-top: 0; padding-bottom: 0; }
.hnameform input { height: 32px; box-sizing: border-box; }
/* A short path (one thing per stage, ≤ 3 stages) as a line: caption over box, an arrow between (charts.js drawFlowLine). */
.fline { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 14px; }
.fline-st { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 0 1 300px; }
.fline-a { color: var(--fg-3); padding-bottom: 8px; }
/*
 * Data store list on a wide card: one set of columns shared by every row, the
 * facts spread across the card like a table's (UI review 3 #10: 1100 px of
 * nothing between the endpoint and its facts at 2560).
 */
@container (min-width: 900px) {
  .sthead { grid-template-columns: 70px minmax(0, 3fr) 90px minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr); }
  .sthead .truncate { max-width: none; }
  .st-m, .st-b, .st-t, .st-svc { justify-self: start; }
  .st-svc { max-width: 100%; }
}
.pname { display: block; }
/* Traces on a phone: the route whole-width with its service under it (the Service column is prio 2). */
.trsvc { display: none; }
@media (max-width: 800px) {
  .trsvc { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .p2-calls td.ly-fill .truncate { display: block; max-width: none; }
}
/* Problem categories: one line-icon set (app.js CAT_SVG), the colour of the text beside it. */
svg.pcat { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; vertical-align: -2px; opacity: .8; flex: none; }
.card h3 svg.pcat { width: 16px; height: 16px; opacity: 1; }
/* A ticket's summary under its title: two lines of the column, not one line cut beside room the fixed columns leave (layoutfill "cutbeside"). */
.ptab .ptitle > .truncate { white-space: normal; max-width: none; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* Ticket log text at the page's size (it was 15px among 12–13px), and the one-event timeline as a line. */
.ptimeline .pevx { font-size: 13px; }
.ptimeline details.pevx > summary { cursor: pointer; }
.card.ptlone { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
.card.ptlone > h3 { margin: 0; }
.ptlone-d { min-width: 0; max-width: 60ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Phone: short facts two to a row, not one tall column (UI review 4 #13: a session's 17 device facts ran 1 200px). */
@media (max-width: 800px) {
  .ly-page .ly-side .devices .grid.g3 { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; gap: 8px; }
  .ly-page .ly-side .devices .grid.g3 > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  .ly-page .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 14px; }
  .ly-page .facts > :has(pre), .ly-page .facts > .wide { grid-column: 1 / -1; }
}
/* API keys: the catalogue as closed groups, one line each (app.js apiKeysView). */
.apicats { display: flex; flex-direction: column; gap: var(--ly-gap); }
.apicats > details.apicat > summary { list-style: none; cursor: pointer; }
.apicats > details.apicat > summary::-webkit-details-marker { display: none; }
.apicats > details.apicat > summary > h3 { margin: 0; }
.apicats > details.apicat > summary > h3::before { content: '▸'; color: var(--fg-3); margin-right: 6px; }
.apicats > details.apicat[open] > summary > h3::before { content: '▾'; }
.apicats > details.apicat[open] > summary { margin-bottom: 10px; }
/* Phone: a business tile is half the row — its field name keeps the room, the type badge goes (the value says it). */
@media (max-width: 800px) { .biztile .bizhead .badge { display: none; } }
/* Device: the window line keeps "Every point", its select and its ? together. */
.iotwin .iotres { display: inline-flex; align-items: center; gap: 6px; }
/* Host head: the actions are an open <details> on a desktop (summary hidden) and a "⋯" menu on a phone. */
.hoststrip .hs-menu { display: contents; }
.hoststrip .hs-menu > summary { display: none; }
.hname-only .hnameedit { opacity: .7; }
@media (max-width: 800px) {
  .hoststrip .hs-menu { display: block; position: relative; margin-left: auto; }
  .hoststrip .hs-menu > summary { display: inline-flex; align-items: center; justify-content: center; list-style: none; height: 32px; min-width: 36px; }
  .hoststrip .hs-menu > summary::-webkit-details-marker { display: none; }
  .hoststrip .hs-menu[open] > .hs-acts { position: absolute; right: 0; top: 38px; z-index: 30; flex-direction: column; align-items: stretch; background: var(--panel); border: 1px solid var(--line-2); border-radius: var(--radius); padding: 8px; box-shadow: var(--shadow); min-width: 200px; }
  body:has(.hoststrip [data-hostsettings]) .topbar .hostgear { display: none; }
}
/* A chart card's pin keeps its corner: it never wraps onto a line of its own. */
.card h3 > .btn[title^="Pin this chart"] { margin-left: auto; flex: none; }
@media (max-width: 800px) {
  .hoststrip { position: relative; padding-right: 44px; }
  .hoststrip .hs-menu { position: absolute; right: 0; top: -4px; margin: 0; }
  .hoststrip .hs-opt { display: none; }
}
.card.techone > .pinrow { flex: 1 1 auto; min-width: 0; }
/* Trends day: our own date on a button; the native input sits under it, invisible, for its picker. */
.trdaypick { position: relative; display: inline-flex; }
.trdaypick > input[type=date] { position: absolute; inset: 0; opacity: 0; pointer-events: none; width: 100%; min-width: 0; }
/* A badge in a table cell is one word-group: it never breaks onto two lines (UI review 4 #38). */
.ly-page td .badge { white-space: nowrap; }
/* A ? keeps a space from the word before it everywhere, not only in headings ("window.?", "service(?)" — UI review 4 polish). */
:is(p, span, div, td, li, summary) > .hintwrap { margin-left: 4px; }
.tlogcard #tlogs > :first-child, .tlogcard #tlogs > :first-child > h3:first-child { margin-top: 0 !important; }
/* Phone: the one filling column's text wraps to two lines instead of being cut to a few letters
   ("dataLayer is n…"); a path keeps its own middle cut (.pth). */
@media (max-width: 800px) {
  .ly-page td.ly-fill .truncate:not(.pth):not(.mono), .ly-page td.ly-fill .ptitle > a {
    white-space: normal; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; max-width: none;
  }
}
@media (max-width: 800px) { .ly-page .p2-day { display: none; } }
/* The keys card beside the how-to (wide): its list or its one line takes the height, centred, not a block at the top. */
.ly-pair > .apikeycard { display: flex; flex-direction: column; }
.ly-pair > .apikeycard > #kl { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
/* Log patterns: the decision says "Always store" (the state beside it says "Stored"), and "proposed" is a tag after the word, not a badge over the button's edge (UI review 4 #34). */
.lpverdict button i { position: static; margin-left: 6px; vertical-align: 1px; }
/* A one-line card built as a heading and its few facts (a one-event timeline, a lone technology entry, a figure read too briefly): one flex line. */
.card.ly-bar:not(:has(> h3 > .row)) { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
@media (max-width: 800px) { .tfig { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 14px; } .tfig > div > b { font-size: 18px; } }
@media (max-width: 800px) { .ctlim { display: block; } }
/* Log patterns: the pair is the state — filled when decided, outlined in the accent when it is only the default. */
.lpverdict button.cur { border-color: var(--accent); color: var(--fg); }
/* Phone: an odd last tile takes the row (2 + 2 + 1 left half a row empty), and a long value wraps on two lines instead of "ord_1d9db…" (UI review 5 #12). */
@media (max-width: 800px) {
  .ly-tiles > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  .biztile .bizval { white-space: normal; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 15px; line-height: 1.25; }
}
/* The small line icons in running text (app.js LINE_ICON / lineIcon). */
svg.licon { width: 1em; height: 1em; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; vertical-align: -0.14em; flex: none; }
.pinbtn { display: inline-flex; align-items: center; gap: 6px; }
.pinbtn .pinlbl { display: none; }
/* In the host's phone "⋯" menu: every item one full-width row, the pin with its words. */
@media (max-width: 800px) {
  .hoststrip .hs-menu[open] > .hs-acts > * { width: 100%; justify-content: flex-start; box-sizing: border-box; }
  .hoststrip .hs-menu[open] > .hs-acts .btn { display: flex; align-items: center; gap: 6px; height: 36px; }
  .hoststrip .hs-menu[open] .pinbtn .pinlbl { display: inline; }
  .hoststrip .hs-menu[open] > .hs-acts > a.small { order: 9; padding: 6px 2px 0; }
}
/* Integrations (Sentry) on the layout system: the connect form one line, the state a line of the card. */
.integ-connect .integ-form { gap: 8px; flex: 1 1 520px; justify-content: flex-end; }
.integ-connect .integ-form input { height: 32px; flex: 1 1 200px; max-width: 320px; }
.integ-state { margin: 0 0 10px; }
/* The card edge is its own token: a touch stronger in light (a 2560 wall of cards on #eceef2), the line in dark. */
.card { border-color: var(--card-line); }
/* API keys: every group's description one line, the whole on hover (IoT's ran two). */
.apicats > details.apicat > summary .drill { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; min-width: 0; flex: 1 1 200px; }
/* The reason reads across its card (it broke at 90ch, ~520px, in an 880px card at 2560 — UI review 5 #17). */
.card .psummary { max-width: none; }
@media (max-width: 800px) {
  .hoststrip .hs-menu[open] > .hs-acts span:has(> .btn) { display: block; width: 100%; }
  .hoststrip .hs-menu[open] > .hs-acts .btn { width: 100%; justify-content: flex-start; box-sizing: border-box; }
}
.tracetopo .tchain { margin-top: 0; } .tracetopo > h3:first-child { margin-top: 0 !important; }

/* An API key's cut: the parts that grant nothing, and the picker that changes it. */
.keycut-dead { text-decoration: line-through; text-decoration-color: var(--bad); }
.keycut-dead .badge { text-decoration: none; display: inline-block; }
.keycut-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 4px 14px; max-height: 220px; overflow-y: auto; }
.keycut-opt { display: flex; gap: 8px; align-items: center; font-size: 13px; margin: 0; overflow-wrap: anywhere; }

/* Kubernetes: a live log or an object's details, and the API connection form. */
.k8slive { min-width: min(880px, 92vw); }
.k8spre { max-height: 62vh; overflow: auto; white-space: pre-wrap; word-break: break-all; font-size: 12px; margin: 0; }
.kint label { display: block; margin-bottom: 8px; }
.kint textarea { width: 100%; font-family: var(--mono, monospace); font-size: 12px; }

/* The click-path recorder (syntheticRecorder in app.js): a picture of the server's browser beside the steps it wrote down.
   Its own layer above every dialog, because it opens over the check form. */
.synrec { position: fixed; inset: 0; z-index: 200; background: var(--bg); color: var(--fg); display: flex; flex-direction: column; }
.synrec-h { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 10px 16px; border-bottom: 1px solid var(--line); background: var(--panel); }
.synrec-t { font-size: 15px; display: inline-flex; align-items: center; gap: 8px; }
.synrec-t::before { content: ''; width: 10px; height: 10px; border-radius: 50%; background: var(--bad); }
.synrec-addr { display: flex; gap: 6px; flex: 1 1 320px; min-width: 0; max-width: 720px; }
.synrec-addr input { flex: 1; min-width: 0; height: 32px; box-sizing: border-box; }
.synrec-h .btn { min-height: 32px; }
.synrec-b { flex: 1; min-height: 0; display: flex; gap: 16px; padding: 14px 16px; overflow: auto; }
.synrec-main { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; }
.synrec-stage { position: relative; background: var(--panel-2); border: 1px solid var(--line-2); border-radius: 6px; overflow: hidden; max-width: 100%;
  width: 640px; height: 400px; box-shadow: var(--shadow); }
.synrec-stage img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; user-select: none; -webkit-user-drag: none; }
.synrec-cap { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; resize: none; outline: none;
  background: transparent; color: transparent; caret-color: transparent; cursor: default; overflow: hidden; touch-action: none; font-size: 16px; }
.synrec-cap:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.synrec-stage.asserting .synrec-cap { cursor: crosshair; box-shadow: inset 0 0 0 3px var(--info); }
.synrec-wait { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center;
  background: var(--panel-2); color: var(--fg-2); font-size: 14px; }
.synrec-wait[hidden] { display: none; }
.synrec-wait.bad { color: var(--bad); }
.synrec-pick { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(320px, 90%); max-height: 80%;
  display: flex; flex-direction: column; gap: 6px; padding: 10px; background: var(--panel); border: 1px solid var(--line-2); border-radius: 8px; box-shadow: var(--shadow-lg); }
.synrec-pick[hidden] { display: none; }
.synrec-opts { overflow: auto; display: flex; flex-direction: column; }
.synrec-opt { text-align: left; padding: 7px 10px; border: 0; border-radius: 6px; background: none; color: var(--fg); font: inherit; cursor: pointer; }
.synrec-opt:hover, .synrec-opt:focus-visible { background: var(--panel-2); }
.synrec-opt.on { background: var(--accent-bg); color: var(--accent-ink); }
.synrec-hint { max-width: 720px; text-align: center; margin: 10px 0 0; }
.synrec-side { flex: 0 0 340px; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.synrec-tools { display: flex; flex-wrap: wrap; gap: 6px; }
.synrec-tools .btn.on { background: var(--info-bg); color: var(--fg); border-color: var(--info); }
.synrec-type { display: flex; gap: 6px; }
.synrec-type input { flex: 1; min-width: 0; height: 32px; box-sizing: border-box; }
.synrec-cookie { font-size: 13px; }
.synrec-sh { margin: 4px 0 0; font-size: 14px; }
.synrec-steps { margin: 0; padding: 0 0 0 22px; overflow: auto; max-height: 46vh; }
.synrec-steps > li { display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--line); }
.synrec-steps > li > .synrec-sw { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; overflow-wrap: anywhere; }
.synrec-steps > li code { font-size: 11px; }
.synrec-steps .btn.sm { min-height: 28px; flex: none; }
.synrec-notes p { margin: 0 0 6px; }
@media (max-width: 900px) {
  .synrec-b { flex-direction: column; }
  .synrec-main { flex: none; }
  .synrec-side { flex: none; }
  .synrec-steps { max-height: none; }
}
@media (max-width: 480px) {
  .synrec-h { padding: 8px 16px; }
  .synrec-addr { flex-basis: 100%; order: 3; }
  .synrec-b { padding: 10px 16px; }
}
