/* amcli web — primitives and patterns.
 *
 * Every colour and every length in this file comes from `tokens.css`. That is
 * not a style preference: `tokens_are_the_only_literals` in
 * crates/amcli-cli/tests/cli.rs fails the build on a hex, an rgb() or a px
 * that is not 0 or 1 anywhere below. The interface drifted last time because
 * nine font sizes, twenty spacings and thirty-seven inline styles were each
 * decided at their own call site; a test is the only thing that stops that
 * happening again.
 */

*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }

body {
  font-family: var(--font);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  color: var(--fg);
  background: var(--surface-0);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}
body.is-dragging, body.is-dragging * {
  user-select: none !important;
  -webkit-user-select: none !important;
  cursor: grabbing !important;
}
body.is-resizing, body.is-resizing * { user-select: none !important; cursor: col-resize !important; }

a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; }
button, input, select, textarea { font: inherit; color: inherit; margin: 0; }
h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: var(--track-tight); }
p { margin: 0; }
[hidden] { display: none !important; }

/* Which letters a search matched, wherever a name is shown while one is being
   typed. A search that matches loosely has to say why a row is there, and it
   says it in the ink already on the row: a hairline under the letters and a
   little more weight. Not a highlighter — a mark lands in a table cell, in a
   palette row and inside a selected row alike, and the palette has no colour
   to spend on it. */
mark { background: none; color: inherit; font-weight: 600; box-shadow: inset 0 -1px 0 currentColor; }

/* One focus treatment, on everything. There was none at all before, and the
   table's sort buttons and the folder tree could not be reached at all. */
:focus { outline: none; }
:focus-visible {
  outline: var(--focus-w) solid var(--focus);
  outline-offset: 1px;
}
/* A full-bleed focusable inside a clipped scroller shows a ring on some edges
   only, so it draws the ring inside itself instead. */
.grid tbody tr:focus-visible { outline-offset: calc(-1 * var(--sp-h)); }
.rail-scope .tree-row:focus-visible,
.th-btn:focus-visible { outline-offset: calc(-1 * var(--focus-w)); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; }
}

/* ---- shared atoms -------------------------------------------------------- */
.spacer { flex: 1 1 auto; min-width: 0; }
.subtle { color: var(--fg-subtle); }
.small { font-size: var(--fs-sm); }
.nowrap { white-space: nowrap; }
.ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.mono { font-family: var(--mono); font-size: var(--fs-sm); }
.num { font-variant-numeric: tabular-nums; }
.right { text-align: right; }

.icon { flex: none; display: block; }
.icon.trail { opacity: 0.65; }
.swatch {
  display: inline-block; flex: none;
  width: var(--sp-2); height: var(--sp-2);
  border-radius: var(--sp-h);
  border: 1px solid var(--line-firm);
}
kbd {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1;
  padding: var(--sp-h) var(--sp-1);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--fg-muted);
  background: var(--surface-1);
  white-space: nowrap;
}
.caps {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--fg-subtle);
}

/* ---- buttons ------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-1);
  height: var(--ctl-h);
  padding: 0 var(--ctl-pad);
  border: 1px solid var(--line-firm);
  border-radius: var(--r-md);
  background: var(--surface-0);
  color: var(--fg);
  font-size: var(--fs-sm);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
/* A control has a height, and a flex parent may not take it away. The rail is
   a column, and on a page whose context fills it the search button was being
   squashed from 26px to 19 — so the same control was two heights on two pages,
   depending on how much was underneath it. */
.btn, .chip, .field, .field-select { flex: none; }
.btn:hover { background: var(--tint); text-decoration: none; }
.btn:active { background: var(--tint-firm); }
.btn[disabled] { opacity: 0.45; cursor: default; }
.btn[disabled]:hover { background: var(--surface-0); }
.btn-icon { width: var(--hit); padding: 0; }
.btn-quiet { border-color: transparent; background: transparent; color: var(--fg-muted); }
.btn-quiet:hover { background: var(--tint); color: var(--fg); }
.btn-primary { background: var(--invert); color: var(--invert-fg); border-color: var(--invert); }
.btn-primary:hover { background: var(--invert); opacity: 0.9; }
.btn.is-active { background: var(--invert); color: var(--invert-fg); border-color: var(--invert); }
.btn.is-active:hover { background: var(--invert); opacity: 0.9; }

.segmented { display: inline-flex; }
.segmented .btn { border-radius: 0; margin-left: -1px; }
.segmented .btn:first-child { border-radius: var(--r-md) 0 0 var(--r-md); margin-left: 0; }
.segmented .btn:last-child { border-radius: 0 var(--r-md) var(--r-md) 0; }
.segmented .btn.is-active { position: relative; z-index: 1; }
/* Segments overlap by a hairline, so the active one covers the shared edge
   of a focused neighbour's ring unless the focused one is lifted higher. */
.segmented .btn:focus-visible { position: relative; z-index: 2; }

/* ---- chips and badges ----------------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  height: var(--ctl-h);
  padding: 0 var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface-0);
  color: var(--fg);
  font-size: var(--fs-sm);
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  max-width: 100%;
}
.chip:hover { background: var(--tint); }
.chip-label { overflow: hidden; text-overflow: ellipsis; }
/* The count keeps its own token so that inverting the chip inverts it too —
   a grey that stayed grey on the inverted ground landed at 2.46:1 in dark. */
.chip-count { color: var(--fg-subtle); font-variant-numeric: tabular-nums; }
.chip-x { opacity: 0.6; }
.chip:hover .chip-x { opacity: 1; }
.chip.is-active { background: var(--invert); color: var(--invert-fg); border-color: var(--invert); }
.chip.is-active .chip-count { color: var(--invert-subtle); }
.chip.is-off .chip-label { text-decoration: line-through; color: var(--fg-muted); }
/* Two controls in one pill: the label does the chip's job, the cross removes.
   The pill keeps its own padding at the ends and the halves carry the rest. */
.chip.is-split { padding: 0; gap: 0; cursor: default; }
.chip-main, .chip-x-btn {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  height: 100%; min-width: 0;
  border: 0; background: transparent; color: inherit; font: inherit;
  cursor: pointer;
  border-radius: var(--r-full);
}
.chip-main { padding: 0 var(--sp-1) 0 var(--sp-2); overflow: hidden; }
.chip-x-btn { padding: 0 var(--sp-2) 0 var(--sp-1); opacity: 0.6; }
.chip-x-btn:hover { opacity: 1; }
.chip.is-split:hover { background: var(--surface-0); }
.chip-main:hover, .chip-x-btn:hover { background: var(--tint); }

.badge {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  max-width: 100%;
  padding: var(--sp-h) var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  color: var(--fg);
  white-space: nowrap;
}
.badge.is-solid { background: var(--invert); color: var(--invert-fg); border-color: var(--invert); }
/* The dismiss inside a badge is the badge's own ink, not a control of its own:
   a bevelled UA button in there made the pill taller than the --ctl-h things
   standing beside it. */
.badge-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--icon); height: var(--icon);
  padding: 0; border: 0;
  border-radius: var(--r-full);
  background: transparent;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
}
.badge-x:hover { opacity: 1; }

/* ---- fields ---------------------------------------------------------------- */
.field {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  height: var(--ctl-h);
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-firm);
  border-radius: var(--r-md);
  background: var(--surface-0);
  min-width: 0;
  /* Set by searchField() as a custom property, not as an inline width, so the
     rule below can widen one that has spilled into the overflow menu. */
  width: var(--field-w, auto);
}
/* The ring belongs on the field and not on the input inside it — the input has
   no border of its own, so a ring there would hug the text rather than the box
   — and it is drawn *inside*, the way a full-bleed table row's is. A field sits
   flush against the toolbar's control rail, which clips its own overflow so
   that nothing spills sideways out of the bar; a ring outside the box was
   shaved off on three edges and left a bracket down the right-hand side. Drawn
   inward it is the same two pixels in the same ink as every other focused
   control, and nothing can trim it. */
.field:focus-within { outline: var(--focus-w) solid var(--focus); outline-offset: calc(-1 * var(--focus-w)); }
.field-icon { color: var(--fg-subtle); }
.field-input {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: transparent; outline: none;
  font-size: var(--fs-sm);
}
.field-input::-webkit-search-cancel-button { cursor: pointer; }
.field-select {
  height: var(--ctl-h);
  padding: 0 var(--sp-6) 0 var(--sp-2);
  border: 1px solid var(--line-firm);
  border-radius: var(--r-md);
  background: var(--surface-0);
  font-size: var(--fs-sm);
  cursor: pointer;
  max-width: 100%;
}
/* Same fade a disabled .btn gets, so a select and a segment switched off
   together read as switched off together. */
.field-select[disabled] { opacity: 0.45; cursor: default; }

/* ---- popover ---------------------------------------------------------------- */
.popover-anchor { position: relative; display: inline-flex; align-items: center; gap: var(--sp-1); flex-wrap: nowrap; }
.popover {
  position: fixed; z-index: var(--z-popover);
  width: calc(var(--rail-w) + var(--sp-6));
  max-height: calc(var(--inspector-max) - var(--sp-12));
  display: flex; flex-direction: column;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
.popover.is-wide { width: var(--palette-w); }
/* 12px, because that is where a checkbox below starts: the list pads 4 and each
   option pads 8 again, and a heading 4px to the left of its own rows reads as a
   mistake rather than as nesting. */
.popover-head {
  display: flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
  flex: none;
  border-bottom: 1px solid var(--line);
}
.popover-title { font-weight: 600; font-size: var(--fs-sm); }
.popover-list { overflow: auto; padding: var(--sp-1); min-height: 0; }
.opt {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sm);
}
.opt:hover { background: var(--tint); }
.opt input { flex: none; margin: 0; accent-color: var(--fg); }
.opt-n { margin-left: auto; color: var(--fg-subtle); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); }

/* ---- the shell --------------------------------------------------------------- */
/* Rail, working surface, inspector — three columns that are always all three.
   The rail is where you narrow: navigation, the folder tree, the filters. The
   middle is what you got. The inspector describes whatever is current.
   Neither side pane closes; each only gets narrower, so the layout never
   jumps and nothing has to be found again.

   Narrow is a state, not a width. `.rail-collapsed` and `.inspector-narrow`
   are the only things that fold a pane, at any window size, so ⌘B and ⌘I can
   always unfold one; a media rule that folded them regardless left the filters
   still filtering with nothing on screen that could have caused it. A small
   window only picks the state the page opens in: app.js reads --fold-inspector
   and --fold-rail once, at load, and folds in the order of what a reader can
   do without — the details of one concept first, then navigation, the filters
   last, because a table you cannot narrow is a table you cannot use. */
#app {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--inspector-w);
  height: 100vh;
  overflow: hidden;
}
#app.rail-collapsed { grid-template-columns: var(--rail-w-min) minmax(0, 1fr) var(--inspector-w); }
#app.inspector-narrow { grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--rail-w-min); }
#app.rail-collapsed.inspector-narrow { grid-template-columns: var(--rail-w-min) minmax(0, 1fr) var(--rail-w-min); }

/* -- rail -- */
#rail {
  position: relative;
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  border-right: 1px solid var(--line);
  background: var(--surface-1);
  overflow: hidden;
}
.rail-head {
  display: flex; align-items: center; gap: var(--sp-2);
  height: var(--toolbar-h); min-height: var(--toolbar-h);
  padding: 0 var(--sp-2);
  border-bottom: 1px solid var(--line);
  flex: none;
}
/* Each pane's fold control sits against the outer edge of the window: the
   rail's leads its header, the inspector's ends its own. A 32px box puts the
   16px icon on the same axis as the icons in the rows below, and a folded pane
   is exactly one such box plus its padding — so leading and trailing are the
   same place there, and neither control moves when its pane folds. It must not
   shrink, and the spacer beside it must not survive the fold, or the gap
   pushes it off that axis. */
#rail-toggle, #inspector-toggle { width: var(--sp-8); flex: none; }
#app.rail-collapsed .rail-head .spacer,
#app.inspector-narrow .inspector-head .spacer { display: none; }
.brand { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.brand-mark { font-weight: 700; font-size: var(--fs-lg); letter-spacing: var(--track-tight); }
.rail-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  padding: var(--sp-2);
  display: flex; flex-direction: column; gap: var(--sp-3);
}
.rail-foot { flex: none; border-top: 1px solid var(--line); padding: var(--sp-2) var(--sp-3); display: grid; gap: var(--sp-h); }
.rail-foot-row { display: flex; align-items: center; gap: var(--sp-2); }

#nav { display: flex; flex-direction: column; gap: 1px; flex: none; }
#nav a {
  display: flex; align-items: center; gap: var(--sp-2);
  height: var(--ctl-h-lg);
  padding: 0 var(--sp-2);
  border-radius: var(--r-md);
  color: var(--fg);
  font-size: var(--fs-md);
}
#nav a:hover { background: var(--tint); text-decoration: none; }
#nav a.is-current { background: var(--invert); color: var(--invert-fg); }
#nav .nav-n { margin-left: auto; font-size: var(--fs-xs); color: var(--fg-subtle); font-variant-numeric: tabular-nums; }
#nav a.is-current .nav-n { color: var(--invert-subtle); }

/* A full-width button in the rail keeps the rail's own inset, not a control's:
   --ctl-pad would start its label 1px right of every label above and below it. */
.rail-search { width: 100%; justify-content: flex-start; padding-inline: var(--sp-2); color: var(--fg-muted); }
.rail-search kbd { margin-left: auto; }

/* What narrows the page, on every page, in one column. */
.rail-context { display: flex; flex-direction: column; gap: var(--sp-4); min-height: 0; flex: 1 1 auto; }
.rail-group { display: flex; flex-direction: column; gap: var(--sp-2); flex: none; min-height: 0; }
.rail-group-grow { flex: 1 1 auto; min-height: calc(var(--sp-12) * 2); }
/* Two scrollable groups sharing what the rail has left. */
.rail-split { display: flex; flex-direction: column; gap: var(--sp-4); min-height: 0; flex: 1 1 auto; }
.rail-group-title { padding: 0 var(--sp-2); }
.rail-scope { overflow-y: auto; overflow-x: hidden; min-height: 0; }

.model-line { display: grid; gap: var(--sp-h); min-width: 0; }
.model-name { font-size: var(--fs-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.model-path { font-family: var(--mono); font-size: var(--fs-xs); color: var(--fg-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.status { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-xs); color: var(--fg-muted); min-width: 0; }
.status .dot { width: var(--sp-2); height: var(--sp-2); border-radius: var(--r-full); background: var(--fg-subtle); flex: none; }
.status.is-live .dot { background: var(--fg); }
.status.is-error { color: var(--alarm); }
.status.is-error .dot { background: var(--alarm); }
.status.is-changed .dot { animation: pulse var(--dur-2) var(--ease) 3 alternate; }
@keyframes pulse { to { transform: scale(1.7); } }

/* Collapsed: icons only, labels on the hover. */
#app.rail-collapsed .brand,
#app.rail-collapsed .rail-foot,
#app.rail-collapsed .rail-context,
#app.rail-collapsed #nav .nav-label,
#app.rail-collapsed #nav .nav-n,
#app.rail-collapsed .rail-search span,
#app.rail-collapsed .rail-search kbd { display: none; }
/* The rows keep their padding and their alignment; only the words go. An icon
   that moves when a pane folds reads as the pane redrawing itself rather than
   resizing, and the rail's width is chosen so that it does not have to. */

/* -- main -- */
#main { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; position: relative; }

/* -- inspector -- */
#inspector {
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  border-left: 1px solid var(--line);
  background: var(--surface-2);
  position: relative;
}
/* The seam between a side pane and the middle. Both panes have one; only the
   side it sits on differs. */
.pane-grip {
  position: absolute; top: 0; bottom: 0;
  width: var(--sp-1);
  cursor: col-resize;
  z-index: var(--z-popover);
}
.pane-grip-left { left: 0; }
.pane-grip-right { right: 0; }
.pane-grip:hover, .pane-grip.is-held { background: var(--line-firm); }
#app.inspector-narrow #inspector-grip,
#app.rail-collapsed #rail-grip { display: none; }
.inspector-head {
  display: flex; align-items: center; gap: var(--sp-2);
  height: var(--toolbar-h); min-height: var(--toolbar-h);
  padding: 0 var(--sp-2);
  border-bottom: 1px solid var(--line);
  flex: none;
}
.inspector-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: var(--sp-4); }
#app.inspector-narrow .inspector-label,
#app.inspector-narrow #inspector-actions,
#app.inspector-narrow .inspector-body { display: none; }

/* ---- toolbar ------------------------------------------------------------------ */
/* Title · meta · controls · trailing, in that order, on every page. It neither
   wraps nor scrolls sideways: what does not fit moves into a menu, because a
   control scrolled out of sight is a control you do not have. */
.toolbar {
  display: flex; align-items: center; gap: var(--sp-2);
  height: var(--toolbar-h);
  min-height: var(--toolbar-h);
  padding: 0 var(--sp-3);
  border-bottom: 1px solid var(--line);
  flex: none;
}
.toolbar-lead { display: flex; align-items: center; gap: var(--sp-1); flex: none; }
.toolbar-title { display: flex; align-items: baseline; gap: var(--sp-2); min-width: 0; flex: none; }
.toolbar-title .icon { align-self: center; color: var(--fg-muted); }
.toolbar-title h1 { font-size: var(--fs-lg); white-space: nowrap; }
.toolbar-meta { font-size: var(--fs-sm); color: var(--fg-subtle); }
/* Stretched to the bar's full height, not sized to the controls in it: the
   rail has to clip sideways — that is what sends a control into the overflow
   menu instead of off the edge — but a rail exactly as tall as a 26px control
   clipped the top and bottom of every focus ring inside it too. */
.toolbar-controls {
  display: flex; align-items: center; gap: var(--sp-2);
  flex: 1 1 auto; min-width: 0;
  align-self: stretch;
  overflow: hidden;
}
.toolbar-controls > * { flex: none; }
/* Nothing in the control row sets its own width from its content: a select is
   as wide as its longest option, and the graph's centre badge
   as wide as the element it is centred on, and either one alone would push the
   rest of the bar into the "more" menu. A ceiling and a min-width of 0 are what
   let the ellipsis inside them do the work instead. */
.toolbar .field-select { max-width: var(--ctl-w); }
.graph-centre { display: inline-flex; align-items: center; min-width: 0; max-width: var(--ctl-w); }
/* A badge is not a control, but in a control row it stands with them. */
.toolbar-controls .badge { height: var(--ctl-h); }
.toolbar-trail { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
.toolbar-more-wrap { margin-left: auto; }
.toolbar-spill {
  position: fixed; z-index: var(--z-popover);
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-2);
  min-width: var(--ctl-w);
  padding: var(--sp-2);
  border: 1px solid var(--line-firm);
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
}
.toolbar-spill > * { width: 100%; }
/* `> *` reaches the .popover-anchor a finder is wrapped in, not the field
   inside it, which would otherwise stand narrow and left-aligned in the stack.
   This wins because the width searchField() asks for is a custom property the
   rule above reads, not an inline width no rule can beat. */
.toolbar-spill .field { width: 100%; }
.toolbar-spill .segmented > .btn { flex: 1 1 auto; }

/* ---- filters ------------------------------------------------------------------ */
/* One idiom, whatever the dimension: a menu that says how much of it is
   showing, and a chip for each value being hidden — because what is hidden is
   the short list and the one worth a click. Stacked, because it lives in the
   rail now. */
.filterbar { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-1); }
.filter-key { font-size: var(--fs-xs); color: var(--fg-subtle); padding: var(--sp-1) var(--sp-2) 0; }
.filter-row { display: flex; align-items: stretch; gap: var(--sp-1); flex-wrap: wrap; min-width: 0; }
.filter-row > .popover-anchor { width: 100%; }
.btn-filter { width: 100%; justify-content: space-between; padding-inline: var(--sp-2); font-variant-numeric: tabular-nums; }
.filter-row > .chip { max-width: 100%; }

/* ---- page ---------------------------------------------------------------------- */
.page { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.page-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }
.page-body.pad { padding: var(--sp-5); }

/* ---- tree ------------------------------------------------------------------------- */
.tree { display: flex; flex-direction: column; gap: 1px; }
.tree-row {
  display: flex; align-items: center; gap: var(--sp-h);
  height: var(--ctl-h);
  padding-right: var(--sp-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sm);
}
.tree-row:hover { background: var(--tint); }
.tree-row.is-active { background: var(--invert); color: var(--invert-fg); }
/* The concept being read, marked exactly as a selected table row is. It is not
   the same thing as `is-active` — that is the folder the tree is narrowed to —
   and one highlight could not honestly mean both. */
.tree-row.is-selected { background: var(--tint-firm); box-shadow: inset var(--sp-h) 0 0 var(--fg); }
.tree-row.is-empty .tree-label { color: var(--fg-subtle); }
.twisty {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tree-indent); height: var(--sp-5);
  padding: 0; border: 0; background: transparent;
  color: var(--fg-subtle); cursor: pointer;
}
.twisty:hover { color: var(--fg); }
.tree-row.is-active .twisty { color: var(--invert-fg); }
.tree-label { flex: 1 1 auto; }
.tree-n { flex: none; font-size: var(--fs-xs); font-variant-numeric: tabular-nums; color: var(--fg-subtle); }
.tree-row.is-active .tree-n { color: var(--invert-subtle); }

/* ---- table --------------------------------------------------------------------- */
.grid-scroll { height: 100%; overflow-y: auto; overflow-x: hidden; }
/* Fixed columns: with `auto`, a nowrap cell sets its own minimum and the table
   pushes the pane sideways. Each column takes its share and long text
   ellipsizes inside it. */
table.grid { border-collapse: separate; border-spacing: 0; width: 100%; table-layout: fixed; }
table.grid th, table.grid td {
  text-align: left;
  padding: 0 var(--sp-3);
  height: var(--row-h);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
table.grid th {
  position: sticky; top: 0; z-index: var(--z-sticky);
  background: var(--surface-1);
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--fg-muted);
  padding: 0;
}
table.grid th.right .th-btn, table.grid td.right { text-align: right; justify-content: flex-end; }
table.grid th > span { display: block; padding: 0 var(--sp-3); overflow: hidden; text-overflow: ellipsis; }
.th-btn {
  display: flex; align-items: center; gap: var(--sp-1);
  width: 100%; height: var(--row-h);
  padding: 0 var(--sp-3);
  border: 0; background: transparent;
  color: inherit; font: inherit;
  cursor: pointer;
  overflow: hidden;
}
.th-btn > span { overflow: hidden; text-overflow: ellipsis; }
.th-btn:hover { background: var(--tint); }
.th-btn:active { background: var(--tint-firm); }
.th-btn.is-sorted { color: var(--fg); font-weight: 600; }
.th-sort { opacity: 0.8; }
/* Rows are one height whatever the content, so the eye can scan a column
   without the rhythm breaking on a folder name that wrapped. */
table.grid tbody tr { cursor: pointer; }
table.grid tbody tr:hover { background: var(--tint); }
table.grid tbody tr.is-selected { background: var(--tint-firm); box-shadow: inset var(--sp-h) 0 0 var(--fg); }
table.grid td .cell {
  display: flex; align-items: center; gap: var(--sp-2);
  min-width: 0;
  overflow: hidden;
}
table.grid td .cell > .ellipsis { min-width: 0; }
table.grid td.right { font-variant-numeric: tabular-nums; color: var(--fg-muted); }
table.grid td.path { font-family: var(--mono); font-size: var(--fs-xs); color: var(--fg-subtle); }
table.grid td.cell-arrow { color: var(--fg-subtle); padding: 0; }
table.grid td.cell-arrow .icon { margin: 0 auto; }
table.grid td.cell-empty { height: auto; white-space: normal; border-bottom: 0; }
.grid-note { padding: var(--sp-2) var(--sp-3); color: var(--fg-subtle); font-size: var(--fs-sm); }

/* ---- hint --------------------------------------------------------------------- */
.hint {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  padding: var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-0);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}
.hint .icon { flex: none; margin-top: var(--sp-h); color: var(--fg-subtle); }

/* ---- empty state ------------------------------------------------------------------- */
.empty {
  display: grid; justify-items: center; gap: var(--sp-2);
  padding: var(--sp-12) var(--sp-4);
  text-align: center;
  color: var(--fg-muted);
}
.empty-icon { color: var(--fg-subtle); }
.empty-title { font-weight: 600; color: var(--fg); }
.empty-body { max-width: var(--read-w); font-size: var(--fs-sm); }
.empty-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; justify-content: center; margin-top: var(--sp-1); }

/* ---- command palette ---------------------------------------------------------------- */
/* The search used to be a 360px dropdown inside a 340px sidebar with
   `overflow: hidden`, so every result's type label was cut off mid-word. It is
   an overlay now — it has the whole window, and it is where ⌘K goes. The veil
   is `--scrim` rather than the page's own surface at an opacity, because over
   a drawing the page under it is white paper. */
.overlay {
  position: fixed; inset: 0; z-index: var(--z-overlay);
  background: var(--scrim);
}
.palette {
  position: fixed; z-index: var(--z-overlay);
  top: var(--sp-12); left: 50%; transform: translateX(-50%);
  width: min(var(--palette-w), calc(100vw - var(--sp-8)));
  max-height: 70vh;
  display: flex; flex-direction: column;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
.palette-field {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-3);
  height: var(--sp-12);
  border-bottom: 1px solid var(--line);
  flex: none;
}
.palette-field input { flex: 1 1 auto; min-width: 0; border: 0; outline: none; background: transparent; font-size: var(--fs-lg); }
.palette-list { overflow: auto; padding: var(--sp-1); min-height: 0; }
.palette-group { padding: var(--sp-2) var(--sp-2) var(--sp-1); }
.palette-hit {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  min-width: 0;
}
.palette-hit:hover, .palette-hit.is-active { background: var(--tint); text-decoration: none; }
/* What a hit is, kept to the right and out of the way. Not scoped to the
   palette: the graph's start-here list is a .link-list and uses the same label,
   and the class has to mean one thing in both idioms. */
.hit-type { margin-left: auto; padding-left: var(--sp-3); color: var(--fg-subtle); font-size: var(--fs-xs); white-space: nowrap; }
.palette-foot {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--line);
  font-size: var(--fs-xs); color: var(--fg-subtle);
  flex: none;
}

/* ---- canvas (a saved view, and the graph) --------------------------------------------- */
.canvas {
  position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden;
  user-select: none; -webkit-user-select: none;
  touch-action: none;
  background: var(--paper);
  background-image:
    linear-gradient(var(--paper-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--paper-grid) 1px, transparent 1px);
  background-size: var(--sp-6) var(--sp-6);
  cursor: grab;
  color: var(--paper-ink);
}
.canvas.is-dragging { cursor: grabbing; }
.canvas > svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.canvas [data-concept], .canvas [data-relationship], .canvas .node, .canvas .edge { cursor: pointer; }
.canvas [data-concept]:hover > *:first-child,
.canvas .node:hover > .figure { filter: brightness(0.95); }
.canvas [data-concept].is-selected > *:first-child,
.canvas [data-relationship].is-selected polyline,
.canvas .node.is-selected > .figure,
.canvas .edge.is-selected line { stroke: var(--paper-ink) !important; stroke-width: 2 !important; }
/* Fading the rest of the graph to near-nothing made it read as broken; enough
   to recede, not enough to vanish. */
.canvas .is-dim { opacity: 0.35; }

/* What floats over the drawing is paper, not chrome. The rule is that anything
   over the drawing must carry BOTH its ground and its ink — `.canvas` sets
   `color: var(--paper-ink)` for the figures, so a panel that inherits that and
   takes a chrome ground is dark ink on a dark ground in the dark theme. That is
   what the legend was: its layer names were invisible. And a chrome ground on
   a sheet that never flips is a near-black hole punched in white paper, which
   is what the zoom cluster and the message box were. So all three take the
   paper's own ground, ink, border and hover, and none of them flips. */
.canvas-hud {
  position: absolute; right: var(--sp-3); top: var(--sp-3); z-index: var(--z-hud);
  display: flex; gap: 1px;
  border: 1px solid var(--paper-line);
  border-radius: var(--r-md);
  background: var(--paper);
  color: var(--paper-ink);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
.canvas-hud .btn { border: 0; border-radius: 0; background: var(--paper); color: var(--paper-ink); }
.canvas-hud .btn:hover { background: var(--paper-tint); }
.canvas-hud .btn + .btn { border-left: 1px solid var(--paper-line); }
.canvas-msg {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: var(--z-hud);
  max-width: calc(var(--palette-w) - var(--sp-8));
  padding: var(--sp-5);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-md);
  background: var(--paper);
  box-shadow: var(--shadow-2);
  color: var(--edge);
  text-align: center;
}
.canvas-msg .empty { padding: 0; color: var(--edge); }
.canvas-msg .empty-icon, .canvas-msg .hit-type { color: var(--edge); }
.canvas-msg .empty-title { color: var(--paper-ink); }
.canvas-msg .btn { background: var(--paper); color: var(--paper-ink); border-color: var(--paper-line); }
.canvas-msg .btn:hover, .canvas-msg .link-list a:hover { background: var(--paper-tint); }
.canvas-msg .btn-primary,
.canvas-msg .btn-primary:hover { background: var(--paper-ink); color: var(--paper); border-color: var(--paper-ink); }
.legend {
  position: absolute; right: var(--sp-3); bottom: var(--sp-3); z-index: var(--z-hud);
  display: grid; grid-template-columns: auto auto; gap: var(--sp-h) var(--sp-3);
  padding: var(--sp-2);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-md);
  background: var(--paper);
  color: var(--paper-ink);
  box-shadow: var(--shadow-2);
  font-size: var(--fs-xs);
}
.legend span { display: inline-flex; align-items: center; gap: var(--sp-1); }

.node text { font-family: var(--font); font-size: var(--fs-xs); fill: var(--paper-ink); pointer-events: none; }
.node .figure { stroke-width: 1; }
.node.is-focus .figure { stroke: var(--paper-ink); stroke-width: 2.5; }
.node.is-pinned .figure { stroke: var(--paper-ink); stroke-width: 2; stroke-dasharray: 4 2; }
.edge line { stroke: var(--edge); stroke-width: 1; }
.edge text { font-family: var(--font); font-size: var(--fs-xs); fill: var(--edge); pointer-events: none; }
.edge .hit { stroke: transparent; stroke-width: 10; }
marker path, marker polygon, marker polyline, marker circle { stroke: var(--edge); }
marker .filled { fill: var(--edge); }
marker .hollow { fill: var(--paper); }

/* ---- concept detail (the inspector, and the reading pane) ------------------------------ */
.detail { display: flex; flex-direction: column; gap: var(--sp-5); }
.detail-head { display: flex; align-items: flex-start; gap: var(--sp-3); }
.detail-head .type-icon { flex: none; }
.detail-title { min-width: 0; display: grid; gap: var(--sp-2); }
.detail-title h1 { font-size: var(--fs-xl); line-height: var(--lh-tight); overflow-wrap: anywhere; }
.detail-meta { display: flex; gap: var(--sp-1); flex-wrap: wrap; align-items: center; }
.detail .actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.sec { display: flex; flex-direction: column; gap: var(--sp-2); }
.doc { white-space: pre-wrap; overflow-wrap: anywhere; max-width: var(--read-w); color: var(--fg-muted); }
table.kv { border-collapse: collapse; width: 100%; }
table.kv th, table.kv td { padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; border-bottom: 1px solid var(--line); vertical-align: top; text-align: left; overflow-wrap: anywhere; }
table.kv th { color: var(--fg-subtle); font-weight: 400; white-space: nowrap; width: 1%; padding-right: var(--sp-4); }

.link-list { display: flex; flex-direction: column; gap: 1px; }
.link-list a, .rel-row {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--ctl-h);
  padding: var(--sp-h) var(--sp-2);
  border-radius: var(--r-sm);
  min-width: 0;
}
/* A row is a <button> when it selects something rather than links to it, and
   the only global button rule resets the font and the colour. Without this the
   UA's own ground and bevel survive, and half the rows in the inspector stand
   1px away from flat ones that look nothing like them. Disabled means the far
   end of the relationship is not in the model. */
.rel-row { width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer; }
.rel-row[disabled] { opacity: 0.45; cursor: default; }
.link-list a:hover, .rel-row:not([disabled]):hover { background: var(--tint); text-decoration: none; }
/* The drawing the rail is standing on, marked the way a chosen folder is. */
.link-list a.is-current { background: var(--invert); color: var(--invert-fg); }
.link-list a.is-current:hover { background: var(--invert); }
.rel-type { color: var(--fg-subtle); font-size: var(--fs-xs); white-space: nowrap; flex: none; }
.rel-arrow { color: var(--fg-subtle); flex: none; }
.id-line { font-family: var(--mono); font-size: var(--fs-xs); color: var(--fg-subtle); overflow-wrap: anywhere; }

/* No colour of its own: the glyph is stroked with currentColor, so it takes the
   ink of whatever it sits in — the invert of a solid badge, the paper inside a
   message on the drawing, the row's ink in a table. Pinning --fg here made it
   white on white inside the graph's centre badge. */
.type-icon { width: var(--icon); height: var(--icon); flex: none; }
.type-icon.boxed {
  width: var(--sp-6); height: var(--sp-6);
  padding: var(--sp-1);
  border-radius: var(--r-sm);
  border: 1px solid var(--line-firm);
}
/* Two things wear .boxed. notation.js builds an <svg> and fills it with the
   type's layer fill, which is pale in both themes, so that one carries the
   paper's ink; the <div> form has no fill of its own, keeps the ground it sits
   on, and has to centre the 16px icon inside a box that is 14px once its border
   and padding are taken off. */
svg.type-icon.boxed { color: var(--paper-ink); }
div.type-icon.boxed { display: inline-flex; align-items: center; justify-content: center; }
div.type-icon.boxed > .icon { width: 100%; height: 100%; }

