/* Gary's dashboard.
 *
 * Light is the base declaration; dark is redefined twice — once for the OS
 * setting and once for an explicit toggle — so the toggle wins in both
 * directions. Every colour is a token: nothing below this block names a hex.
 *
 * The lane palette (--s1…--s6) is the validated categorical set. Slots are
 * assigned in lane order and never cycled, so no two lanes on screen share a
 * colour, and the ordering is the one that clears colour-blind separation on
 * adjacent pairs — which is exactly how lanes are stacked.
 */

:root {
  color-scheme: light;

  --page: #f9f9f7;
  --surface: #fcfcfb;
  --surface-2: #f2f1ed;
  --ink: #0b0b0b;
  --ink-2: #52514e;
  --ink-3: #898781;
  --rule: #e1e0d9;
  --rule-2: rgba(11, 11, 11, 0.1);
  /* Lighter than --rule. The waterfall stacks a dozen horizontal lines and a dozen at --rule
     reads as a table; this is the weight that separates rows without drawing them. */
  --hair: #ecece4;

  --s1: #2a78d6;
  --s2: #eb6834;
  --s3: #1baf7a;
  --s4: #eda100;
  --s5: #e87ba4;
  --s6: #008300;

  --good: #0ca30c;
  --good-text: #006300;
  --warn: #fab219;
  --serious: #ec835a;
  --bad: #d03b3b;
  --info: #2a78d6;
  /* Landed. Its own token rather than `--good`, because it is the whole surface of a merged
     card — pill, strip, tint and border all read this one value, which is what makes a done
     card look like one thing rather than a green pill on a green card in two greens.
     Deliberately a colder green than `--good`'s, so a phase that passed and a run that landed
     are still distinguishable side by side. */
  --merged: #13a151;

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --page: #0d0d0d;
    --surface: #1a1a19;
    --surface-2: #232322;
    --ink: #ffffff;
    --ink-2: #c3c2b7;
    --ink-3: #898781;
    --rule: #2c2c2a;
    --rule-2: rgba(255, 255, 255, 0.1);
    --hair: #232322;

    --s1: #3987e5;
    --s2: #d95926;
    --s3: #199e70;
    --s4: #c98500;
    --s5: #d55181;
    --s6: #008300;

    --good-text: #0ca30c;
    --info: #3987e5;
  --merged: #5cc98c;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --page: #0d0d0d;
  --surface: #1a1a19;
  --surface-2: #232322;
  --ink: #ffffff;
  --ink-2: #c3c2b7;
  --ink-3: #898781;
  --rule: #2c2c2a;
  --rule-2: rgba(255, 255, 255, 0.1);
  --hair: #232322;

  --s1: #3987e5;
  --s2: #d95926;
  --s3: #199e70;
  --s4: #c98500;
  --s5: #d55181;
  --s6: #008300;

  --good-text: #0ca30c;
  --info: #3987e5;
  --merged: #5cc98c;
}

/* Each lane sets --lane once; every mark inside it reads that. */
.s1 { --lane: var(--s1); }
.s2 { --lane: var(--s2); }
.s3 { --lane: var(--s3); }
.s4 { --lane: var(--s4); }
.s5 { --lane: var(--s5); }
.s6 { --lane: var(--s6); }

/* The waterfall's geometry, and the only place it is written down.
 *
 * One row per density, four numbers each: how wide the canvas is, how tall a lane is, how tall a
 * chip is, and the narrowest a chip may be drawn. `dashboard.js` reads the same properties, so the
 * table is not duplicated in the script. `--chat` is here for the same reason: the drawer's width
 * is part of the page's geometry, and shutting it is one property changing. */
:root { --canvas: 1980px; --row: 68px; --chip: 46px; --chip-min: 34px; --chat: 392px; --d-left: 340px; }
:root[data-density="compact"] { --canvas: 1180px; --row: 42px; --chip: 28px; --chip-min: 30px; }
:root[data-density="standard"] { --canvas: 1980px; --row: 68px; --chip: 46px; --chip-min: 34px; }
:root[data-density="detailed"] { --canvas: 3200px; --row: 104px; --chip: 82px; --chip-min: 40px; }
:root[data-chat="shut"] { --chat: 0px; }

.ic { display: block; flex: none; }

/* ---- the run page: a layout, not a document ----
 *
 * The one screen here that is an application rather than a page. A fixed header, three regions
 * that scroll independently, nothing below the fold — which is what makes "the chart, the phase
 * and the conversation at once" possible at all. Every other page keeps the ordinary flow, and the
 * class is what switches between them.
 */
body.run-page { height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
body.run-page .top { position: static; flex: none; padding: 9px 16px; }
body.run-page .flash { flex: none; }

/* Margins on all four sides, and the same 16px gutter the header rows use. `.bar` carries
 * `margin: 14px 26px 0` for the document pages, and on this one the missing bottom margin put a
 * red "not accepted" banner flush against the axis directly under it — two unrelated things
 * touching, which reads as one broken thing. */
body.run-page .flash .bar,
body.run-page .run-top .bar { margin: 10px 16px 12px; }

body.run-page main {
  flex: 1;
  min-height: 0;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--chat);
  grid-template-rows: auto minmax(0, 1fr) auto;
}

/* The polled region is not a box, and must not become one: a wrapper around these three is a
 * fourth thing to give a height to. `display: contents` makes its children the page's own grid
 * items, which is what lets the composer — a sibling, deliberately outside the poll — sit in the
 * drawer's foot without being inside the element the poll replaces. */
#trace { display: contents; }
.run-top { grid-area: 1 / 1 / 2 / 3; min-width: 0; }
.run-main { grid-area: 2 / 1 / 4 / 2; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.drawer { grid-area: 2 / 2 / 3 / 3; }
body.run-page .composer { grid-area: 3 / 2 / 4 / 3; }

[data-chat="shut"] .drawer,
[data-chat="shut"] body.run-page .composer { display: none; }
[data-chat="shut"] .run-main { grid-area: 2 / 1 / 4 / 3; }

/* The desktop panel, when there is one: the bottom edge of the left column, flush.
 *
 * It used to be *inside* `.run-main`, which is inside `#trace` — and `#trace` is the polled
 * region whose `innerHTML` is reassigned every 1.5 seconds. That detaches the iframe, which
 * discards its browsing context by specification, so the desktop painted once and then went
 * black for good. Nothing in CSS could fix that; the panel had to leave the region.
 *
 * **It does not get a grid row of its own, and the first version's did.** Giving the panel row 3
 * put it in the *composer's* row — and a grid row is as tall as the tallest thing in it, so a
 * 38px bar sat at the top of a cell sized to a 110px textarea and left seventy pixels of white
 * underneath. That is not a margin anybody can tune away: the height comes from the other side
 * of the page. Reported from a live run, and invisible in a preview whose composer was short.
 *
 * **And it costs nothing to any row, which is why it is positioned rather than placed.**
 * `align-self: end` in row 3 fixes the white space under the bar and leaves the other half: a
 * 320px drawer makes row 3 320px tall, and the *conversation* then ends early with a gap above
 * the composer opposite. Every grid item contributes to its row's height, so any placement in a
 * shared row trades one column's problem for the other's. Absolutely positioned, the panel
 * contributes to nothing.
 *
 * `right: var(--chat)` is the left column's own edge — the same variable that sizes the second
 * track, so the panel is exactly as wide as the chart and follows the conversation being dragged
 * without a second number to keep in step. The track survives `[data-chat="shut"]`, which is why
 * that case needs its own `right: 0` rather than inheriting anything.
 *
 * `.run-main` reserves the room with `padding-bottom`, so nothing is hidden behind the panel
 * despite the overlap.
 *
 * Flush: no side margin, no bottom margin, no radius, and the panel's own `border-top` is the
 * only line. It is the bottom of the pane rather than a card floating in it, which is what the
 * design drew and what the first version's rounded box got wrong. */
body.run-page.has-desktop main { position: relative; }
body.run-page.has-desktop .rig { position: absolute; left: 0; right: var(--chat); bottom: 0; margin: 0; }
[data-chat="shut"] body.run-page.has-desktop .rig { right: 0; }
body.run-page.has-desktop .run-main { padding-bottom: 38px; }

/* Open, the reserved room is the drawer's own height — the value the grip writes. Same number in
   both places by construction: the drawer *is* `var(--desk)` tall, so the chart gives up exactly
   that and no pixel of the waterfall is hidden behind it. */
[data-desk="open"] body.run-page.has-desktop .run-main { padding-bottom: var(--desk, 320px); }

/* A session is its desktop, so the desktop takes the page: one column, no waterfall, and the drawer
   open whatever the reader's saved preference, with nothing to collapse it into. `.run-main` is
   hidden rather than removed because it carries `.rig-mark`, which is how the page notices the
   desktop changing state and reloads. Launching needs only that the frame has a layout, so this is
   also what starts the stream on load. */
body.session-page main { grid-template-columns: minmax(0, 1fr); }
body.session-page .run-main { display: none; }
body.session-page.has-desktop .rig {
  position: static;
  grid-area: 2 / 1 / 4 / 2;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
body.session-page .rig-bar,
body.session-page .rig-hide,
body.session-page [data-grip="desk"] { display: none; }
body.session-page .rig-drawer {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  height: auto;
  position: relative;
  padding-top: 8px;
  background: var(--surface-2);
  border-top: 1px solid var(--rule);
}

/* A session's activity panel: shut unless asked, and when open it takes the column the
   conversation drawer would, at the same width, with the desktop giving up exactly that. Its own
   attribute rather than `data-chat`, so shutting the conversation elsewhere does not hide it. */
.activity { display: none; }
[data-activity="open"] body.session-page main { grid-template-columns: minmax(0, 1fr) var(--chat, 380px); }
[data-activity="open"] body.session-page .activity {
  grid-area: 2 / 2 / 4 / 3;
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  background: color-mix(in srgb, var(--page) 55%, var(--surface));
  border-left: 1px solid var(--rule);
}
.activity-open {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 30px;
  padding: 0 11px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 11.5px;
  cursor: pointer;
  white-space: nowrap;
}
.activity-open:hover { background: var(--surface-2); color: var(--ink); }
[data-activity="open"] .activity-open { display: none; }

.act { display: flex; flex-direction: column; gap: 4px; }
.act .turn-who { margin-bottom: 0; }
.act .turn-name { color: var(--ink-2); font-weight: 600; }
.act-message .turn-name { color: var(--s1); }
.act-text { font-size: 12.5px; line-height: 1.5; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.act-message .act-text {
  padding: 9px 11px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--s1) 8%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--s1) 22%, var(--surface));
}
.act-tool .act-text { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); }
.act-desktop .act-text, .act-presence .act-text { color: var(--ink-2); }
.act-note { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }
* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font: 14px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
/* `a` above inherits its colour, which is right for a card or a row that is entirely a link
   and wrong for a url sitting inside a sentence: it was already an anchor on the recorded
   rail and looked exactly like the text beside it, so nobody clicked it. `linkify` stamps
   this, and it is the only thing in the app that reads as a link in the web's own idiom. */
.url { color: var(--info); text-decoration: underline; text-underline-offset: 2px; }
.url:hover { color: var(--ink); }
.m { font-family: var(--mono); }
.dim { color: var(--ink-3); }
.num { font-variant-numeric: tabular-nums; text-align: right; }
.empty { color: var(--ink-3); font-style: italic; padding: 22px 26px; }

/* ---- top bar ---- */

.top {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  /* Baseline, not centre.
     `center` lines up the boxes, and the boxes are different heights: the brand is as
     tall as its 28px mark, the crumbs are a line of 12px mono. Their centres coincided
     and their baselines did not, which is the specific look of type being a pixel or two
     out — visibly wrong without being obviously wrong. The eye reads baselines.
     Everything on the right is centred instead, since none of it is type that should sit
     on the same line as the crumbs. */
  align-items: baseline;
  gap: 16px;
  padding: 12px 26px;
  background: color-mix(in srgb, var(--page) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--rule);
}

/* Deliberately not a flex container. A flex item's baseline comes from its own first
   baseline-participating child, and a centred image participates in nothing — so a flex
   brand hands the header the bottom edge of the mark as its "baseline". As a block with
   inline content it hands over the baseline of the word, which is the thing being
   aligned to. */
.brand { font-weight: 700; letter-spacing: -0.01em; white-space: nowrap; }

/* Explicit width and height on the element as well as the tag, so the header does
   not reflow when the image lands. `middle` keeps the mark optically level with the
   word while leaving the word's baseline in charge. */
.brand-mark { width: 28px; height: 28px; vertical-align: middle; margin-right: 9px; }

.crumbs { display: flex; align-items: baseline; gap: 8px; font-family: var(--mono); font-size: 12px; color: var(--ink-2); }
.crumbs a:hover { color: var(--ink); text-decoration: underline; }
.crumbs .sep { color: var(--ink-3); }

/* Centred within the header, not baseline-aligned with the crumbs: a toggle, a status
   dot and an avatar have nothing to sit on a text baseline for. */
.top-right { margin-left: auto; align-self: center; display: flex; align-items: center; gap: 16px; }

.theme { display: inline-flex; border: 1px solid var(--rule); border-radius: 999px; overflow: hidden; }
.theme-btn {
  border: 0;
  background: transparent;
  color: var(--ink-3);
  padding: 3px 9px;
  font-size: 12px;
  cursor: pointer;
  line-height: 1.6;
}
.theme-btn:hover { color: var(--ink); background: var(--surface-2); }
.theme-btn[aria-pressed="true"] { color: var(--ink); background: var(--surface-2); }

.live { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.1em; }
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px color-mix(in srgb, var(--good) 60%, transparent); }
.live.stale .live-dot { background: var(--ink-3); box-shadow: none; }
body.polling .live-dot { animation: pulse 1.6s ease-in-out infinite; }

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes spin { to { transform: rotate(360deg); } }
/* An expanding halo, for the one dot that means work is happening this second. `pulse` is the
   quieter neighbour and is reused rather than duplicated as a second fade: a blocked run's dot
   breathes, a live one radiates, and the difference between "waiting" and "working" is the
   difference between those two — which is the whole point of having both. */
@keyframes ring {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--info) 45%, transparent); }
  70%, 100% { box-shadow: 0 0 0 5px transparent; }
}
/* Travelled with `background-position` rather than a translated child, so the sweep needs no
   overlay element and no `overflow: hidden` on the strip. Clipping it there would have cut the
   1px a hovered segment grows past its container, which is the only feedback the strip gives. */
@keyframes sweep {
  from { background-position: -30% 0; }
  to { background-position: 130% 0; }
}

/* Motion here is information, not decoration — it is the only thing distinguishing a run that
   is working from one that has stopped — so it degrades to the static treatments rather than
   to nothing. A running card still has its border, its ring and its pill. */
@media (prefers-reduced-motion: reduce) {
  .live-dot, .state-pill .pill-dot, .live-dots span, .phases.running::after,
  .block.running, .chip.running .chip-i {
    animation: none !important;
  }
}

main { padding: 0 0 70px; }

/* ---- the board: what is on screen, and the controls that say so ----
 *
 * Metrics, controls and the set filters share one surface and one bottom rule because they
 * are one statement. The old page had totals for the whole factory sitting above a filtered
 * grid, so the numbers and the cards were about different things and nothing said so.
 */

.board {
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  padding: 18px 20px 0;
}

.metrics {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 12px 36px;
}

.metric { text-decoration: none; color: inherit; }
.metric-v {
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.metric-sub { font-family: var(--mono); font-size: 11px; font-weight: 400; letter-spacing: 0; color: var(--info); }
.metric-k {
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}

/* The one metric that is a to-do list rather than a fact. It changes colour only when it is
   not zero, so a page with nothing waiting is quiet rather than reassuring. */
/* Not `--warn` itself: amber on a light surface is unreadable as text. The same mix the
   waiting pill uses for its own label, so the number and the pill it filters to match. */
.metric-do.on .metric-v,
.metric-do.on .metric-sub { color: color-mix(in srgb, var(--warn) 45%, var(--ink)); }
.metric-do:hover .metric-k { color: var(--ink); }
.metric-do:hover .metric-v { text-decoration: underline; text-underline-offset: 3px; }

/* ---- the segmented switchers ----
 *
 * Range and scope are the same control at two sizes: a padded track with the selected option
 * lifted out of it on a white tile. One set of rules, two sizes, because two hand-written
 * copies of this drift.
 */

.ranges, .scopes {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.ranges { margin-left: auto; margin-bottom: 4px; }
.scopes { border-radius: 9px; }

.range, .scope {
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  color: var(--ink-3);
  text-decoration: none;
  white-space: nowrap;
  border-radius: 6px;
}
.range { height: 26px; padding: 0 12px; font-size: 11.5px; }
.scope { height: 34px; padding: 0 16px; font-size: 12.5px; border-radius: 7px; }

.range:hover, .scope:hover { color: var(--ink); }
.range.on, .scope.on {
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
}
.scope.on { font-weight: 500; }

/* ---- the control bar ---- */

.controls {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 0 14px;
}

/* Below this the bar cannot hold a 620px search field beside two other controls without
   crushing it, so the search takes its own row rather than shrinking to uselessness. */
@media (max-width: 800px) {
  .controls { flex-wrap: wrap; }
  .seek { order: 3; flex-basis: 100%; max-width: none; }
}

/* The paste box, made the size of the thing people came here to use. It was 230px and last
   in a row of chips. */
.seek { position: relative; display: flex; align-items: center; flex: 1; max-width: 620px; }
.seek-i { position: absolute; left: 14px; color: var(--ink-3); pointer-events: none; }
.seek-in {
  width: 100%;
  height: 42px;
  padding: 0 74px 0 42px;
  font-size: 13.5px;
  font-family: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule-2);
  border-radius: 10px;
}
.seek-in::placeholder { color: var(--ink-3); }
.seek-in:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ink) 6%, transparent);
}
/* A label for a key, not a button. */
.seek-key {
  position: absolute;
  right: 12px;
  height: 20px;
  padding: 0 7px;
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-3);
  background: var(--page);
  border: 1px solid var(--rule);
  border-radius: 5px;
  pointer-events: none;
}
.seek-in:not(:placeholder-shown) ~ .seek-key { display: none; }
.seek-clear {
  position: absolute;
  right: 44px;
  font-family: var(--mono);
  font-size: 15px;
  color: var(--ink-3);
  text-decoration: none;
}
.seek-clear:hover { color: var(--ink); }

/* ---- everything that grows ----
 *
 * A `<details>`, not a scripted menu, and that is load-bearing: open-ness is DOM state, so the
 * keyboard and the live poll's `restoreSections` both already handle it. A hand-rolled popover
 * would be slammed shut by the innerHTML swap 1.5 seconds after being opened.
 */

.filters { position: relative; margin-left: auto; }
/* The session button takes the push to the right edge, so it and Filters sit together. */
.new-session { margin-left: auto; background: var(--surface); }
.new-session + .filters { margin-left: 0; }
/* The Activity link leads the right-hand group, so whatever follows it stops pushing. */
.activity-link { margin-left: auto; background: var(--surface); text-decoration: none; }
.activity-link + .new-session,
.activity-link + .filters { margin-left: 0; }

.session-dialog {
  width: min(460px, calc(100vw - 32px));
  padding: 22px 22px 18px;
  border: 1px solid var(--rule-2);
  border-radius: 12px;
  background: var(--surface);
  color: var(--ink);
}
.session-dialog::backdrop { background: rgba(16, 20, 24, 0.45); }
.session-form { display: grid; gap: 10px; }
.session-form h2 { margin: 0; font-size: 18px; }
.session-what { margin: 0 0 6px; color: var(--ink-2); font-size: 13.5px; line-height: 1.5; }
.session-form .keys-input { width: 100%; }
.session-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.session-go { border-color: var(--ink); color: var(--ink); }
.filters-b {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 42px;
  padding: 0 14px;
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--ink-2);
  border: 1px solid var(--rule-2);
  border-radius: 10px;
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.filters-b::-webkit-details-marker { display: none; }
.filters-b:hover { background: var(--surface-2); }
.filters-b.on { border-color: var(--ink); color: var(--ink); }
.filters-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--ink);
  color: var(--surface);
  font-size: 10px;
}
.filters-v { opacity: 0.5; }
.filters[open] .filters-v { transform: rotate(180deg); }

.facets {
  position: absolute;
  top: 46px;
  right: 0;
  z-index: 90;
  width: 560px;
  max-width: calc(100vw - 40px);
  padding: 16px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 12px;
  box-shadow: 0 16px 40px color-mix(in srgb, var(--ink) 16%, transparent);
}
.facets-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
.facet { min-width: 0; }
.facet-h {
  margin: 0 0 8px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}
.facet-find {
  width: 100%;
  height: 30px;
  margin-bottom: 4px;
  padding: 0 8px;
  font-size: 12px;
  font-family: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 7px;
}
.facet-find:focus { outline: none; border-color: var(--rule-2); }
.facet-none { margin: 6px 8px; font-size: 12.5px; color: var(--ink-3); }
.facet-list { list-style: none; margin: 0; padding: 0; }
/* The growth fix, in one declaration: a new colleague extends this box, not the page. */
.facet-list.scrolls { max-height: 188px; overflow-y: auto; }

.opt {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 8px;
  border-radius: 7px;
  font-size: 12.5px;
  color: var(--ink-2);
  text-decoration: none;
}
.opt:hover { background: var(--surface-2); color: var(--ink); }
.opt.on { color: var(--ink); }
.opt-box {
  flex: 0 0 15px;
  width: 15px;
  height: 15px;
  border: 1px solid var(--rule-2);
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--surface);
}
.opt.on .opt-box { background: var(--ink); border-color: var(--ink); }
.opt-l { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opt-n { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }

.facets-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--hair);
}
.facets-help { flex: 1; font-size: 11.5px; color: var(--ink-3); }
.facets-clear, .facets-done {
  height: 30px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  font-size: 11px;
  border-radius: 7px;
  text-decoration: none;
  cursor: pointer;
}
.facets-clear { color: var(--ink-2); background: none; border: 1px solid var(--rule); }
.facets-done { color: var(--surface); background: var(--ink); border: 1px solid var(--ink); }

/* ---- the filters that are set ----
 *
 * Rendered only when there are some. This is the trade the popover bought: a set filter is
 * visible and removable, and an unset one costs no chrome at all — where four rows of chips
 * were on screen permanently to say that nothing was selected.
 */

.chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 0 0 14px; }
.chip-off {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding: 0 7px 0 10px;
  font-family: var(--mono);
  font-size: 11px;
  background: var(--page);
  border: 1px solid var(--rule-2);
  border-radius: 7px;
  text-decoration: none;
}
.chip-off .chip-k { color: var(--ink-3); }
.chip-off .chip-v { color: var(--ink); }
.chip-off .ic { color: var(--ink-3); }
.chip-off:hover { border-color: var(--ink); }
.chip-off:hover .ic { color: var(--ink); }
.chip-clear { font-family: var(--mono); font-size: 11px; color: var(--ink-3); text-decoration: none; }
.chip-clear:hover { color: var(--ink); }

/* ---- the list, under the day it happened ----
 *
 * Grouping is the answer to a question the flat grid could not be asked. Five near-identical
 * NoMethodError runs read as five separate problems until you can see they all happened on
 * one afternoon.
 */

.list { padding: 0 20px 40px; }

.day {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 18px 2px 10px;
  /* A gradient, not a fill: cards scroll *under* the header rather than appearing to be
     clipped by a band. */
  background: linear-gradient(var(--page) 72%, transparent);
}
.day-h { margin: 0; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-2); }
.day:first-child .day-h { color: var(--ink); }
.day-m { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.day-rule { flex: 1; height: 1px; background: var(--hair); }

/* 352px, not the old 430px five-up: the old grid left no room for a readable subject line,
   which is now the headline. */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(352px, 1fr)); gap: 14px; }

.pager { display: flex; justify-content: center; margin-top: 22px; }
.pager-link {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 7px 16px;
  text-decoration: none;
}
.pager-link:hover { color: var(--ink); border-color: var(--rule-2); background: var(--surface); }

.nothing { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 90px 0; }
.nothing-h { margin: 0; font-size: 15px; font-weight: 600; }
.nothing-p { margin: 0; font-size: 13px; color: var(--ink-2); }
.nothing-q { font-family: var(--mono); color: var(--ink); }
.nothing-go {
  height: 32px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--surface);
  background: var(--ink);
  border-radius: 8px;
  text-decoration: none;
}

/* ---- the card ----
 *
 * Still no coloured left edge. What changed is the rest of that rule, which used to read "no
 * state-tinted border, state is the dot and the pill" — and it was right about the wrong
 * number. Tinting all seven states does colour the page's structure rather than its content;
 * that was measured and it is why the edge bar was rejected. Tinting *two* does the opposite.
 *
 * Three questions have to answer themselves from the shape of a grid of twenty, before a word
 * is read: this one is finished, this one is working right now, this one needs me. So the two
 * endpoints of the scale take a surface — done and running — and the five in the middle stay
 * neutral, which is what leaves them a field for the other two to stand out of. The third
 * question is the pill, which now leads the ident line instead of sitting under a subject of
 * varying height.
 *
 * Motion is reserved for work in flight, in one direction only: running radiates, blocked
 * breathes, stopped is still. So movement on this page always means somebody or something is
 * still due to act, and stillness always means nothing is.
 */

.card {
  display: flex;
  flex-direction: column;
  padding: 13px 15px 12px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 12px;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: border-color 150ms, box-shadow 150ms;
}

/* Declared before `:hover` on purpose: these tie it on specificity, so source order is what
   decides, and a card must still respond to the cursor whatever state it is in. */
.card.merged {
  background: color-mix(in srgb, var(--merged) 6%, var(--surface));
  border-color: color-mix(in srgb, var(--merged) 28%, var(--rule));
}
.card.running {
  border-color: color-mix(in srgb, var(--info) 35%, var(--rule));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--info) 8%, transparent);
}

.card:hover {
  border-color: var(--rule-2);
  box-shadow: 0 3px 12px color-mix(in srgb, var(--ink) 7%, transparent);
}
/* The ring is the state and the shadow is the cursor, so hovering a live run must not cost it
   the thing that says it is live. Both, rather than one replacing the other. */
.card.running:hover {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--info) 8%, transparent),
    0 3px 12px color-mix(in srgb, var(--ink) 7%, transparent);
}

.ident { display: flex; align-items: center; gap: 8px; }
/* The one item on this row allowed to give. The pill and the owner tag are whole words that
   mean nothing cut in half — see the `.foot` note below, where that lesson cost a pill reading
   "wai" — and a ticket is still recognisable from its first characters. */
.ident-t { font-family: var(--mono); font-size: 12.5px; font-weight: 700; letter-spacing: -0.01em; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ident-k {
  font-family: var(--mono);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}
.ident .owner { margin-left: auto; }
.ident-b { margin-top: 3px; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }

.subject {
  margin-top: 9px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink);
  text-wrap: pretty;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ---- the phase strip ----
 *
 * `flex: 1` per segment is why there is no cap and no "+34 more": a 62-phase run fits the
 * same card a 6-phase run does. The dot matrix this replaced could not, and needed two
 * separate truncation limits to pretend otherwise.
 */

.phases { display: flex; gap: 2px; height: 7px; margin-top: 13px; }
.seg { flex: 1; min-width: 2px; border-radius: 2px; background: var(--rule); transition: height 120ms; }
.seg.succeeded { background: color-mix(in srgb, var(--good) 45%, var(--surface)); }
.seg.skipped { background: var(--rule); }
.seg.failed { background: var(--bad); }
.seg.running { background: var(--info); }
/* The last segment takes the run's own colour when the ending is the point. A merged run's
   tail is the same green as the rest of it, which is what makes the two that need looking at
   findable. */
.seg.stalled { background: var(--serious); }
.seg.unproven { background: var(--warn, #b26a00); }
.phases:hover .seg { opacity: 0.55; }
.phases .seg:hover { height: 9px; margin-top: -1px; background: var(--ink); opacity: 1; }
.phases-none { margin-top: 13px; font-size: 12.5px; font-style: italic; color: var(--ink-3); }

/* A finished run's strip goes fully saturated. The 45% mix is the right weight for one green
   band among six other colours; on a card where every band is that green it reads as washed
   out, and "washed out" is the one thing a done run must not look like. Fading completed work
   was the previous instinct here and it worked against the only job this card has. */
.card.merged .seg.succeeded { background: var(--merged); }
.card.merged .seg.skipped { background: color-mix(in srgb, var(--merged) 22%, var(--surface)); }

/* An indeterminate sweep, because the remaining phase count is unknown — the strip cannot show
   progress towards an end it does not know. `pointer-events: none` so the segments underneath
   keep their hover, which is what supplies the caption. */
.phases.running { position: relative; }
.phases.running::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 3px;
  pointer-events: none;
  background-image: linear-gradient(90deg, transparent, color-mix(in srgb, var(--info) 55%, transparent), transparent);
  background-size: 30% 100%;
  background-repeat: no-repeat;
  animation: sweep 1.7s linear infinite;
}

.caption { margin-top: 7px; font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
/* At rest the caption is coloured by the ending, because that is what it is describing. Only
   two endings earn a colour: one that landed and one that did not. Declared above `.hovered`
   so that the hovered line — which is about a segment, not about the run — wins the tie. */
.caption.merged { color: var(--good-text); }
.caption.failed { color: var(--bad); }
.caption.hovered { color: var(--ink-2); }

/* Everything in here is rigid and the row wraps instead.
 *
 * Without `flex: 0 0 auto` the pill is the item that gives: it is the shortest, so proportional
 * shrinking takes it first, and "waiting on us" rendered as "wai" on a 440px card. A state word
 * that has been cut in half is worse than no state word — "unp" and "not" are not English. The
 * card minimum is 352px and this row can want ~400px of it, so on the narrowest card the stats
 * drop to a second line, which is legible in a way a clipped pill is not. */
.foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 9px;
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px solid var(--hair);
}
.foot > * { flex: 0 0 auto; }
.foot-n { margin-left: auto; }
.foot-n, .foot-t { font-family: var(--mono); font-size: 10.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.foot-n { color: var(--ink-2); }
/* Tinted towards the card it sits on, so the stats settle into a done card instead of being the
   one neutral thing on it. The subject deliberately does *not* get this treatment: muting
   finished work is the instinct here and it works against the only job the card has. */
.card.merged .foot-n { color: color-mix(in srgb, var(--merged) 25%, var(--ink-2)); }
.foot-t { color: var(--ink-3); }
/* The spacing either side is the template's own, so this only colours the mark. Padding here as
   well cost 24px a card, which is most of the difference between one line and two. */
.foot-x { color: var(--ink-3); }
/* Styled as a chip and deliberately not a control: a button or anchor nested in the card's own
   anchor is invalid HTML that browsers resolve by dropping one of them.
 *
 * In the state's own colours rather than the one blue it used to be. `Retry` on a failed run and
 * `Review` on one waiting for a reader are different errands, and colouring them alike meant the
 * footer said "do something" while the pill three lines up said what — two glances for one fact. */
.foot-do {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 22px;
  padding: 0 9px;
  border-radius: 6px;
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  color: var(--ink-2);
  background: var(--surface-2);
}
.foot-do.waiting, .foot-do.stalled { background: color-mix(in srgb, var(--warn) 16%, var(--surface)); color: color-mix(in srgb, var(--warn) 45%, var(--ink)); }
.foot-do.failed { background: color-mix(in srgb, var(--bad) 12%, var(--surface)); color: color-mix(in srgb, var(--bad) 72%, var(--ink)); }
.card:hover .foot-do { text-decoration: underline; text-underline-offset: 2px; }

/* What is happening this second, on the one card where anything is. The strip says how far and
   the pill says that it is live; this says who, which is the fact somebody wanting to interrupt
   actually needs. Not repeated in the caption above — a running card said the agent's name twice
   before this existed. */
.live-note { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 10.5px; color: var(--info); }
.live-dots { display: inline-flex; gap: 3px; }
.live-dots span { width: 3px; height: 3px; border-radius: 50%; background: var(--info); animation: pulse 1.1s ease-in-out infinite; }
.live-dots span:nth-child(2) { animation-delay: 0.18s; }
.live-dots span:nth-child(3) { animation-delay: 0.36s; }

/* ---- one word per state ----
 *
 * The pill and the dot are the same seven colours, mixed into the surface for the fill so the
 * pair holds in both themes without fourteen more tokens.
 */

/* Deliberately not `.pill`, which is taken. The run page's link row uses that name for a
   30px-wide collapsed icon that widens to its label on hover — `max-width` and
   `overflow: hidden` — and it is declared later in this file, so a card's state pill inherited
   the collapse and rendered "waiting on us" as "wai". */
.state-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 20px;
  padding: 0 8px;
  border-radius: 6px;
  font-family: var(--mono);
  font-size: 10.5px;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* The dot the ident line used to carry on its own, now inside the word it was qualifying —
   which is what lets it animate without a lone speck of movement floating beside the text.
   `currentColor` so the seven pills need no eighth rule each. */
.pill-dot { flex: 0 0 6px; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* Keyed on the state name, never on the tone. `unproven` wears `waiting`'s amber deliberately,
   and it is also genuinely a run whose next step is a person — so it breathes with the rest of
   them. `failed` gets nothing: a run that has stopped must be still, or movement stops meaning
   anything. */
.state-pill.running .pill-dot { background: var(--info); animation: ring 1.6s ease-out infinite; }
.state-pill.waiting .pill-dot,
.state-pill.stalled .pill-dot,
.state-pill.unproven .pill-dot { animation: pulse 2.4s ease-in-out infinite; }

.state-pill.running { background: color-mix(in srgb, var(--info) 12%, var(--surface)); color: color-mix(in srgb, var(--info) 70%, var(--ink)); }
.state-pill.waiting, .state-pill.stalled { background: color-mix(in srgb, var(--warn) 16%, var(--surface)); color: color-mix(in srgb, var(--warn) 45%, var(--ink)); }
.state-pill.failed { background: color-mix(in srgb, var(--bad) 12%, var(--surface)); color: color-mix(in srgb, var(--bad) 72%, var(--ink)); }
/* Amber for the same reason `.chip.unproven` is. The pill reads its class from the state's
   `tone`, which is `waiting` now, so this rule is a backstop rather than the live path — kept
   and repointed rather than deleted, because a stale `serious` here is a trap for whoever
   changes that tone next. */
.state-pill.unproven { background: color-mix(in srgb, var(--warn) 16%, var(--surface)); color: color-mix(in srgb, var(--warn) 55%, var(--ink)); }
/* The only solid fill on the page, and that is the mechanism rather than a flourish: every
   unfinished state is a pale tint, so "filled" reads as "finished" before the word under it is
   read at all.
   It was purple, on the argument that green already means "this phase passed" and a run that
   landed is an outcome rather than a pass. True, and it cost more than it bought: the one card
   on the page whose whole surface says *done* wore the only colour on it that says nothing
   else, so the badge fought the card. The colder green of `--merged` keeps the distinction the
   purple was making — a passed phase is `--good`, a landed run is this — without the card
   reading as two unrelated things. */
.state-pill.merged {
  background: var(--merged);
  /* `--surface` and not white, because this is the one pill whose *background* is a token that
     flips with the theme: `--merged` is a mid green in light mode and a light one in dark, so
     fixed white text lands at about 1.8:1 on the dark theme. `--surface` flips the other way and
     stays the readable choice on both. Same trick as `.nothing-go`. */
  color: var(--surface);
  font-weight: 700;
  /* Uppercase here and on no other pill, because this is the only ending that is *over*. The
     quiet states are sentences a person is meant to read ("waiting on us"); this one is a stamp
     they are meant to spot from across a grid of twenty. */
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0 9px 0 7px;
}
/* `--ink-2`, not `--ink-3`: the faint grey is 3.2:1 on this fill, which is under AA for text
   this size. Every other pill here clears 5:1, and the quiet states should not be the two a
   reader cannot read. */
.state-pill.idle { background: var(--surface-2); color: var(--ink-2); }

/* ---- chips ---- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 10px 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--rule);
  font-size: 11.5px;
  white-space: nowrap;
}
.chip-i { font-size: 11px; }
.chip.ok { color: var(--good-text); border-color: color-mix(in srgb, var(--good) 40%, var(--rule)); }
.chip.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, var(--rule)); }
/* `--warn`, not `--serious`. A refused run is somebody's turn, not a fault: it has a branch, a
   pull request and a verdict, and the only thing between it and finished is a reader. Painting it
   the colour of a crash is what made adw-20260824-0beb — green suite, green CI, one stale gate —
   look broken at a glance. Amber is the register the rest of this app uses for "waiting on us". */
.chip.unproven { color: var(--warn, #b26a00); border-color: color-mix(in srgb, var(--warn, #b26a00) 45%, var(--rule)); }
.chip.running { color: var(--info); border-color: color-mix(in srgb, var(--info) 45%, var(--rule)); }
/* Stalled: the shape of running, none of the motion, and a warning colour. It has to be
 * distinguishable at a glance from both a live run and a failed one, because it is neither. */
.chip.stalled { color: var(--warn, #b26a00); border-color: color-mix(in srgb, var(--warn, #b26a00) 45%, var(--rule)); }
.chip.running .chip-i { display: inline-block; animation: spin 1.4s linear infinite; }
.chip.idle { color: var(--ink-3); border-style: dashed; }

.stat {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-size: 11.5px;
  white-space: nowrap;
}
.stat-i { color: var(--ink-3); font-size: 10.5px; }
.stat-v { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.stat.compact { border: 0; padding: 0; color: var(--ink-2); }

/* One run's cost, all in, with the three-way split behind a native <details>.
   No JS: the disclosure is the element's own, and the :hover rule below opens the same panel so
   click and hover both work. The panel is absolutely positioned so opening one in the run grid
   does not reflow the cards around it. */
.cost { position: relative; display: inline-block; }
.cost > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-size: 11.5px;
  white-space: nowrap;
}
.cost > summary::-webkit-details-marker { display: none; }
.cost.compact > summary { border: 0; padding: 0; color: inherit; font-size: inherit; }
.cost-i { color: var(--ink-3); font-size: 10.5px; }
.cost-v { font-family: var(--mono); font-variant-numeric: tabular-nums; }

.cost-panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  left: 0;
  min-width: 208px;
  padding: 9px 11px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 14%);
  font-size: 11.5px;
  text-align: left;
  white-space: nowrap;
}
/* Closed by default; shown when the element is open (click/keyboard) or hovered. Two selectors
   rather than a JS controller. */
.cost:not([open]) .cost-panel { display: none; }
.cost:hover .cost-panel { display: block; }
/* Near the right edge of the grid the panel would overflow the viewport. One flag on the call
   site flips it rather than a measurement in JS. */
.cost.right .cost-panel { left: auto; right: 0; }

.cost-rows { list-style: none; margin: 0; padding: 0; }
.cost-row { display: flex; justify-content: space-between; gap: 18px; padding: 2px 0; }
.cost-k { color: var(--ink-2); }
.cost-n { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.cost-note {
  margin: 7px 0 0;
  padding-top: 6px;
  border-top: 1px solid var(--rule);
  color: var(--ink-3);
  font-size: 10.5px;
  white-space: normal;
  max-width: 232px;
}

.tag { display: inline-flex; align-items: baseline; gap: 5px; font-family: var(--mono); font-size: 10.5px; }
.tag-k { color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.07em; }
.tag-v { color: var(--ink-2); }

/* ---- run head ----
 *
 * Two rows, split by *kind of thing* rather than by size. Row one is what this run is and what to
 * do about it; row two is where else it lives and how to read the chart below. Nothing on row one
 * is a preference and nothing on row two is an action — which is what makes the row a person needs
 * findable. Before this it was one row that grew: a title, five metrics, three external links and
 * however many moves the run happened to be offering, all wrapping into each other.
 */
.run-head { flex: none; background: var(--surface); border-bottom: 1px solid var(--rule); }
.run-head-row { display: flex; align-items: flex-start; gap: 16px; padding: 12px 16px 9px; }
.run-head-row-2 { align-items: center; flex-wrap: wrap; gap: 10px 12px; padding: 0 16px 11px; }
.run-fixes { margin: 0 16px 11px; padding: 9px 12px; border: 1px solid var(--warn); border-radius: 6px; background: var(--surface-2); font-size: 13px; }
.run-fixes ul { margin: 6px 0 0; padding-left: 18px; }
.run-fixes li { font-family: var(--mono); font-size: 11.5px; overflow-wrap: anywhere; }

.run-ident { flex: 1; min-width: 0; }
.run-ident-top { display: flex; align-items: center; gap: 10px; min-width: 0; }
.run-title { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.run-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 9px; margin-top: 7px; font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); }
.run-meta-id { color: var(--ink); }

/* Drawn rather than typed. A `·` written into the markup between every pair is a separator that
 * survives one of its neighbours being absent, which is how a meta line ends up starting with a
 * dot on a run that has no branch. */
.run-meta > * + *::before { content: "·"; margin-right: 9px; color: var(--rule-2); }

.run-links { display: flex; align-items: center; gap: 6px; flex: none; }

/* Collapsed to its icon until somebody looks at it. The row costs 30px per link rather than the
 * width of a label, which is what lets a fourth integration append here instead of pushing the
 * metrics onto a third row. `:focus-visible` as well as `:hover`: a pill that only opens for a
 * mouse is a pill a keyboard reader has to guess at. */
.pill {
  display: inline-flex;
  align-items: center;
  flex: none;
  height: 28px;
  max-width: 30px;
  padding: 0 7px;
  border: 1px solid var(--rule);
  border-radius: 7px;
  background: var(--surface);
  color: var(--ink-2);
  overflow: hidden;
  white-space: nowrap;
  transition: max-width 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.pill:hover, .pill:focus-visible { max-width: 280px; background: var(--surface-2); border-color: var(--rule-2); color: var(--ink); }
/* The pill that is a button: open from the start, because an action folded to an icon is one
   nobody finds. `display: contents` so `button_to`'s form does not become a flex item of its own. */
.pill-form { display: contents; }
.pill-create { max-width: 280px; font: inherit; cursor: pointer; }
.pill-label { padding-left: 8px; font-family: var(--mono); font-size: 11px; }

.run-rule { flex: none; width: 1px; height: 20px; background: var(--rule); }
.run-stats { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; min-width: 0; }
.run-head-right { margin-left: auto; flex: none; display: flex; align-items: center; gap: 8px; }

/* How much of the waterfall to show at once. A preference about the reader rather than a fact
 * about the run, so it is client-side and remembered. */
.density { display: inline-flex; gap: 2px; padding: 3px; background: var(--surface-2); border: 1px solid var(--rule); border-radius: 8px; }

.density-btn {
  height: 24px;
  padding: 0 11px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
}

.density-btn:hover { color: var(--ink); }
.density-btn[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent); }

.bar { margin: 14px 26px 0; padding: 9px 13px; border-radius: 8px; font-size: 13px; }
.bar.bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 8%, transparent); border: 1px solid color-mix(in srgb, var(--bad) 30%, transparent); }
.bar.ok { color: var(--good-text); background: color-mix(in srgb, var(--good) 9%, transparent); border: 1px solid color-mix(in srgb, var(--good) 30%, transparent); }
.bar.warn { color: var(--ink-2); background: color-mix(in srgb, var(--serious) 9%, transparent); border: 1px solid color-mix(in srgb, var(--serious) 32%, transparent); }
/* A question for the reader, in --info rather than a warning colour. It is not a problem
   with the run; it is the run's turn to be answered, and painting it like a failure would
   teach people to skim past the one banner that is addressed to them. */
.bar.ask { color: var(--ink-2); background: color-mix(in srgb, var(--info) 9%, transparent); border: 1px solid color-mix(in srgb, var(--info) 34%, transparent); }
.bar.ask code { font-family: var(--mono); font-size: 12px; }
/* Which half of the machine, on its own line under the heading. Quieter than the reason above it
   on purpose: it is orientation for somebody meeting Gary for the first time, and it should not
   compete with the sentence telling a regular what to do. */
.gate-stage { display: block; margin-top: 4px; color: var(--ink-3); font-size: 12.5px; }
.bar.ask ul.steers { margin: 6px 0 0; padding-left: 18px; }
.bar.ask ul.steers li { margin: 3px 0; }

/* ---- the waterfall ----
 *
 * A run with thirty phases and a run with three want different charts, and nothing about the run
 * decides which — it depends on whether somebody is scanning for a shape or reading a step. So the
 * geometry is four custom properties the density choice sets, and every rule below reads them.
 *
 * **Widths are pixels; positions are not.** A percentage cannot express "make this chip wide enough
 * for the word `investigate`", and that is precisely what goes wrong in a dense lane: a phase that
 * took four seconds is a two-pixel sliver, and a sliver with no room for a label is a square
 * nobody can identify. So `dashboard.js` computes widths, because it needs the rendered width of a
 * label — a fact only the browser holds.
 *
 * `left` stays the percentage the template wrote and no script touches it, because that percentage
 * *is* the timeline. Growing a chip and then nudging its neighbour along to make room turns a
 * dense lane into a contiguous wall in run order, which loses the one thing the horizontal axis is
 * for. Overlap is prevented by clamping the width instead — see `pack()`.
 */
.fall { flex: 1; min-width: 0; min-height: 0; background: var(--surface); }

.fall-scroll { position: relative; height: 100%; overflow: auto; overscroll-behavior: contain; }

/* Every scroller the poll rewrites, which is what `[data-scroll]` marks — the chart, the
 * conversation, both panes of the phase sheet and every `<pre>` in it. Two reasons, and the
 * second is why this is not just the chart's rule any more.
 *
 * An overlay scrollbar that fades out leaves no indication the chart continues, and a run wide
 * enough to need scrolling is exactly the run whose right-hand end matters.
 *
 * And an overlay scrollbar *fades in* on a programmatic scroll and on a content resize — both of
 * which `dashboard.js` does to every one of these elements every 1.5 seconds: the swap gives each
 * scroller fresh content at scrollTop 0, then `restoreScroll` and `restoreFollow` put the reader's
 * offset back. So a running run made every bar on the page flash in and fade out on the poll's
 * clock, which reads as the page twitching rather than as anything about the run. A bar that is
 * always there cannot flash. The chart had this already and was the one pane not doing it. */
[data-scroll]::-webkit-scrollbar { height: 11px; width: 11px; }
[data-scroll]::-webkit-scrollbar-track { background: var(--surface-2); }

[data-scroll]::-webkit-scrollbar-thumb {
  background: var(--ink-3);
  border-radius: 999px;
  border: 3px solid var(--surface-2);
}

[data-scroll]::-webkit-scrollbar-thumb:hover { background: var(--ink-2); }
/* Firefox, which has no ::-webkit pseudo-elements and its own two properties — and the
 * `@supports` is load-bearing rather than tidy, because these two properties *disable* every rule
 * above them.
 *
 * Since Chrome 121 an element that sets `scrollbar-width` or `scrollbar-color` is styled by the
 * standard machinery and its `::-webkit-scrollbar` rules are ignored outright, and the standard
 * machinery on macOS is the overlay scrollbar that fades. So this line, added for a browser
 * nobody here uses, had quietly switched the chart's persistent scrollbar back off — the comment
 * above it claimed a bar that stays visible while `.fall-scroll` was measuring a 0px gutter in
 * Chrome 152. A fix that a fallback reproduces the defect of is a fix nobody switched on.
 *
 * Measured rather than read off the spec: three probe divs in a real Chrome, one with the webkit
 * rules alone (an 11px gutter, so a classic bar), one with both (0px, so an overlay one), one
 * with this `@supports` around the pair (11px). Chrome supports `::-webkit-scrollbar` as a
 * selector, so it skips the block; Firefox does not, so it gets its two properties. */
@supports not selector(::-webkit-scrollbar) {
  [data-scroll] { scrollbar-width: thin; scrollbar-color: var(--ink-3) var(--surface-2); }
}

/* While a drag is panning the chart, and only then. `cursor: grab` on the scroller at rest would
 * fight the pointer over every chip, which is a link and should say so. The chips need naming
 * because their own `pointer` wins over an inherited cursor, and the selection guard is because
 * dragging across a chart otherwise highlights every lane name it passes. */
body.panning,
body.panning .fall-scroll,
body.panning .block { cursor: grabbing; }
body.panning { user-select: none; }

/* Both rows size to their own content rather than to the scroll container, and never narrower
 * than it — so a short run still fills the panel and a long one scrolls. */
.fall-axis, .lane { display: flex; width: max-content; min-width: 100%; }

.fall-axis { position: sticky; top: 0; z-index: 4; height: 34px; background: var(--surface-2); border-bottom: 1px solid var(--hair); }

/* Both heads stay put while the track scrolls under them — a block whose lane name has scrolled
 * off the left is a block nobody can attribute. The axis head is above both, since it is the one
 * cell where the sticky row and the sticky column cross. */
.axis-head, .lane-head { position: sticky; left: 0; flex: none; width: 208px; z-index: 3; border-right: 1px solid var(--rule); }

.axis-head {
  z-index: 5;
  display: flex;
  align-items: center;
  padding: 0 14px;
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}

/* One coordinate space for the whole track. The zone used to be a flex sibling that took its
 * width out of the axis, so a tick's percentage and a block's percentage were percentages of two
 * different boxes — the axis and the chips under it agreeing only when the request zone was zero
 * wide. Both are absolute over the same canvas now. */
.axis-body { position: relative; flex: 1 0 var(--canvas); }
.axis-track { position: absolute; inset: 0; }
.axis-track .tick { position: absolute; bottom: 9px; transform: translateX(-50%); font-family: var(--mono); font-size: 10px; color: var(--ink-3); white-space: nowrap; }

/* Before the factory did anything: the ticket sitting in somebody's queue. Its own zone rather
 * than part of the clock, because on a ticket that waited a week it is the whole chart. */
.zone-head {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 8px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  border-right: 1px solid var(--rule);
}

/* Every lane, including the last one. The last one used to have its border removed, which is
 * right for a list inside a box and wrong for a chart: the lanes are shorter than the panel on
 * any run with a handful of actors, so the rows simply stopped and the ruled grid bled into
 * whitespace with nothing to say the chart had ended. `--rule` rather than `--hair` on the last
 * one, because it is closing the table rather than separating two rows of it. */
.lane { border-bottom: 1px solid var(--hair); }
.lane:last-child { border-bottom: 1px solid var(--rule); }

.lane-head {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  height: var(--row);
  padding: 0 14px;
  background: var(--surface);
  overflow: hidden;
}

.lane-name { font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--lane); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lane-count, .lane-sub { font-family: var(--mono); font-size: 10px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* At 42px there is room for the name and the phase count and nothing else. Hidden rather than
 * squeezed: a clipped model id reads as a rendering fault. */
[data-density="compact"] .lane-sub,
[data-density="compact"] .ctx { display: none; }

.ctx { display: flex; align-items: center; gap: 6px; margin-top: 3px; }
.ctx-bar { flex: 1; min-width: 0; height: 3px; border-radius: 2px; background: var(--hair); overflow: hidden; }
.ctx-fill { height: 100%; background: var(--lane); transition: width 300ms ease; }
.ctx-v { flex: none; font-family: var(--mono); font-size: 9.5px; color: var(--ink-3); }

/* Alternating, faintly. Twelve lanes of one colour is twelve chances to read a chip against the
 * wrong name. The head is left alone — it is sticky, and a stripe that scrolls under type is
 * worse than no stripe. */
.lane-track { position: relative; flex: 1 0 var(--canvas); height: var(--row); }
.lane:nth-of-type(odd) .lane-track { background: color-mix(in srgb, var(--page) 30%, var(--surface)); }

.grid { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px solid var(--hair); }
.zone-line { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px solid var(--rule); }

.block {
  position: absolute;
  top: calc((var(--row) - var(--chip)) / 2);
  height: var(--chip);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 8px;
  border-radius: 8px;
  overflow: hidden;
  background: color-mix(in srgb, var(--lane) 11%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--lane) 32%, transparent);
  color: var(--lane);
  transition: box-shadow 0.15s, transform 0.15s;
}

.block:hover { box-shadow: 0 0 0 1px var(--lane), 0 6px 18px color-mix(in srgb, var(--lane) 22%, transparent); z-index: 2; }
.block.selected { box-shadow: 0 0 0 2px var(--lane); z-index: 2; }
.block.failed { border-color: color-mix(in srgb, var(--bad) 70%, transparent); background: color-mix(in srgb, var(--bad) 10%, var(--surface)); color: var(--bad); }
.block.running { animation: pulse 1.8s ease-in-out infinite; }
/* `running` on the row and nothing executing it. Deliberately not the pulse — the animation is
   what said "this is happening" while adw-20260821-6090 sat still — and deliberately not the
   failure red, because nothing has failed and a requeued job may yet pick the run up. */
.block.stalled { border-color: color-mix(in srgb, var(--warn) 55%, transparent); color: var(--warn); }

/* ---- a stage that did not run ----
 *
 * Dashed and unfilled. A skipped phase's *status* is `succeeded` — recording the decision was the
 * job and it did it — so anything reading status alone drew a green tick, and `merge ✓` beside an
 * open pull request is the specific lie this fixes. */
.block.skipped {
  background: transparent;
  border-style: dashed;
  border-color: color-mix(in srgb, var(--lane) 38%, transparent);
  color: color-mix(in srgb, var(--lane) 70%, var(--surface));
}

.b-top { display: flex; align-items: center; gap: 7px; min-width: 0; }
.b-i { display: inline-flex; flex: none; }
.b-name { font-family: var(--mono); font-size: 11.5px; font-weight: 500; color: inherit; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-time { margin-left: auto; flex: none; font-family: var(--mono); font-size: 10px; opacity: 0.6; }
.b-desc { display: none; }

/* Too narrow for the word. The glyph centres and everything else goes, rather than showing the
 * first three letters of a name — `inv`, `int` and `ini` are not different labels. `dashboard.js`
 * decides which chips get this, because whether a label fits is a measurement. */
.block.icon-only { padding: 0; align-items: center; justify-content: center; }
.block.icon-only .b-name,
.block.icon-only .b-time,
.block.icon-only .b-desc { display: none; }

/* Narrower than the glyph it would hold. Showing the middle strip of a 14px icon in an 11px box
 * reads as a rendering fault; a solid tick reads as "a phase, too small to name", which is what it
 * is. The chip is still hoverable, and the peek card is where its name lives. */
.block.bare { background: color-mix(in srgb, var(--lane) 55%, var(--surface)); }
.block.bare .ic,
.block.bare .ttick { display: none; }

[data-density="compact"] .block { padding: 0; align-items: center; justify-content: center; }
[data-density="compact"] .b-name,
[data-density="compact"] .b-time { display: none; }

[data-density="detailed"] .block { justify-content: flex-start; padding-top: 8px; }
[data-density="detailed"] .b-desc {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 5px;
  font-size: 11px;
  line-height: 1.35;
  color: var(--ink-2);
}

[data-density="detailed"] .block.icon-only { justify-content: center; padding-top: 0; }

.ttick { position: absolute; bottom: 4px; width: 2px; height: 7px; border-radius: 1px; background: var(--lane); opacity: 0.55; }
.ttick.err { background: var(--bad); opacity: 1; }
[data-density="compact"] .ttick { display: none; }

/* ---- the hover peek ----
 *
 * One card for the whole chart rather than one per chip: thirty hidden cards is thirty layouts the
 * browser does for nothing, and the card has to escape its lane's bounds anyway. Everything it
 * shows travels on the chip as data attributes, so hovering costs no request.
 *
 * The one-line summary is the load-bearing part. A chip says which step this was; the summary says
 * what the step concluded, which is the question somebody scanning a lane of thirteen is asking. */
.peek {
  position: absolute;
  z-index: 60;
  width: 308px;
  padding: 12px 13px;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 11px;
  box-shadow: 0 14px 32px color-mix(in srgb, var(--ink) 16%, transparent);
}

.peek-head { display: flex; align-items: center; gap: 8px; }
.peek-kind { font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--lane); }
.peek-agent { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }
.peek-dur { margin-left: auto; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }
.peek-body { margin: 8px 0 0; font-size: 12px; line-height: 1.45; color: var(--ink-2); text-wrap: pretty; }
.peek-foot { display: flex; align-items: baseline; gap: 10px; margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--hair); font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
.peek-open { margin-left: auto; color: var(--ink-2); }

/* ---- phase detail ----
 *
 * Docked under the waterfall in the same column. It used to be a card below the chart, which
 * meant opening a phase pushed the thing you clicked off the top of the screen.
 *
 * The height is a custom property rather than a constant because 330px is a guess about what
 * somebody is doing: reading a transcript wants most of the screen, glancing at a gate result
 * wants almost none. `--sheet` is set on the root from `localStorage` before first paint — the
 * same trick and the same reason as the density and the drawer, since all three change the
 * page's geometry and applying one from the deferred script lays the chart out twice.
 */
.detail {
  flex: none;
  position: relative;
  height: var(--sheet, 330px);
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface);
  border-top: 1px solid var(--rule);
  box-shadow: 0 -8px 24px color-mix(in srgb, var(--ink) 6%, transparent);
}

/* The drag handle, over the panel's own top border.
 *
 * Tall enough to hit without being a band of dead space, and it draws itself only on hover or
 * focus — a permanent grip line is a second border competing with the real one. Focusable and
 * arrow-key operable, because a control that exists only for a pointer is a control half the
 * people here cannot use. */
.d-grip {
  position: absolute;
  top: -3px;
  left: 0;
  right: 0;
  height: 9px;
  z-index: 3;
  cursor: ns-resize;
  display: flex;
  align-items: center;
  justify-content: center;
}
.d-grip::after {
  content: "";
  width: 42px;
  height: 3px;
  border-radius: 2px;
  background: transparent;
  transition: background 120ms;
}
.d-grip:hover::after, .d-grip:focus-visible::after { background: var(--ink-3); }
.d-grip:focus-visible { outline: none; }
/* Same handle, turned ninety degrees: the description column's right edge, and the
   conversation's left one. Everything about the interaction is shared — one script, one grip
   table — so what is left here is which way it points. */
.v-grip {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 9px;
  z-index: 3;
  cursor: ew-resize;
  display: flex;
  align-items: center;
  justify-content: center;
}
.v-grip::after {
  content: "";
  width: 3px;
  height: 42px;
  border-radius: 2px;
  background: transparent;
  transition: background 120ms;
}
.v-grip:hover::after, .v-grip:focus-visible::after { background: var(--ink-3); }
.v-grip:focus-visible { outline: none; }

/* Sat on the boundary each one owns, so the handle is where the line is. */
.d-grip-desc { left: calc(var(--d-left) - 4px); }
.d-grip-chat { left: -4px; }
.drawer { position: relative; }

/* While the drag is happening, everywhere — the pointer leaves the grip immediately and the
   cursor has to keep saying what is going on. Two classes because there are two axes now, and a
   `ns-resize` cursor on a sideways drag is the interface lying about what it is doing. */
body.sizing, body.sizing * { cursor: ns-resize !important; }
body.sizing-x, body.sizing-x * { cursor: ew-resize !important; }
body.sizing, body.sizing-x { user-select: none; }

.d-head { flex: none; height: 44px; display: flex; align-items: center; gap: 10px; padding: 0 14px; border-bottom: 1px solid var(--hair); }
.d-name { font-family: var(--mono); font-size: 13px; font-weight: 700; }
.d-tags { margin-left: auto; display: flex; gap: 14px; }

.d-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 7px;
  color: var(--ink-3);
  font-size: 14px;
}

.d-close:hover { color: var(--ink); background: var(--surface-2); }
.detail > .bar { flex: none; margin: 9px 14px 0; }

/* `position: relative` is for the grip alone: an absolutely positioned child takes no grid
   column, which is what lets the divider sit *on* the boundary rather than beside it. */
.d-grid { flex: 1; min-height: 0; position: relative; display: grid; grid-template-columns: var(--d-left) minmax(0, 1fr); }
.d-left { overflow: auto; padding: 10px 14px 16px; border-right: 1px solid var(--hair); }
.d-right { overflow: auto; padding: 4px 16px 16px; }
/* On `:root`, not on `.d-grid`. A dragged width is an inline property on the root element, and a
   `.d-grid` rule here would be more specific than the variable it is meant to be a default for —
   so the narrow-window default would silently win over what somebody chose. */
@media (max-width: 1100px) { :root { --d-left: 260px; } }

.sect { border-bottom: 1px solid var(--rule); }
.sect:last-child { border-bottom: 0; }
.sect > summary { cursor: pointer; padding: 10px 0; display: flex; align-items: baseline; gap: 8px; list-style: none; }
.sect > summary::-webkit-details-marker { display: none; }
.sect > summary::before { content: "▸"; color: var(--ink-3); font-size: 9px; }
.sect[open] > summary::before { content: "▾"; }
.sect-t { font-family: var(--mono); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
.sect-body { padding: 0 0 14px; }

.d-desc { margin: 0; color: var(--ink-2); }

.usage { width: 100%; max-width: 380px; border-collapse: collapse; font-size: 12.5px; }
.usage th { font-family: var(--mono); font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); font-weight: 400; text-align: left; padding: 0 0 5px; }
.usage td { padding: 3px 0; }
.usage td.num { font-family: var(--mono); }
.u-total td { border-top: 1px solid var(--rule); font-weight: 700; padding-top: 6px; }
.u-note { margin: 9px 0 0; font-size: 11.5px; color: var(--ink-3); max-width: 46ch; }

/* The recorded-values table carries paths and urls, not the four short numbers
   the cost table was sized for, so it gets the width and is allowed to wrap. */
.usage.kv { max-width: none; }
.usage.kv td:first-child { color: var(--ink-3); white-space: nowrap; padding-right: 14px; vertical-align: top; }
.usage.kv td:last-child { word-break: break-all; }

/* A link that has to sit among plain metadata without turning the row into a
   navigation bar: it reads as text until you go near it. */
.tick-link { border-bottom: 1px solid var(--rule); }
.tick-link:hover { color: var(--ink); border-bottom-color: currentColor; }

/* The one thing on the sign-in page addressed to somebody who does not have an account yet.
 * Quieter than the sign-in button, which is what almost everybody who lands here wants, and
 * distinct enough not to read as fine print — a newcomer has to notice it or they are back to
 * being walked through the setup by a colleague. */
.signin-start {
  margin-top: 18px;
  padding: 12px 14px;
  border: 1px solid var(--rule-2);
  border-radius: 10px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-2);
  text-align: left;
}
.signin-start strong { color: var(--ink); }

/* Its own line, and `anywhere` rather than `break-all`: the second breaks a url at whatever
 * character lands at the edge — "htt p://gary.test/llms.txt" — and somebody copying it by eye
 * copies the break. This one only breaks when there is genuinely nowhere else to go. */
.signin-start-url {
  display: block;
  margin-top: 8px;
  font-size: 12px;
  overflow-wrap: anywhere;
  /* `a { color: inherit }` is this stylesheet's default, which is right for a card that is
   * itself a link and wrong here: a newcomer needs to be able to tell this is something to
   * click as well as something to copy. */
  color: var(--info);
}

/* ---- whose plate this is on ----
 *
 * A name, not a handle and not an email. Same partial on the card and the run page, so the
 * two cannot come to look like different facts.
 *
 * `text-transform: none` because .card-flow is uppercased and "JON" reads as shouting rather
 * than as a person. The pill is deliberately quiet — this has to be findable by scanning a
 * column of twenty cards, which wants alignment and contrast, not colour. */
.owner {
  font-family: var(--mono);
  font-size: 11px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink-2);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border-radius: 999px;
  padding: 1px 7px;
  white-space: nowrap;
}

/* The same chip, as a control. A button rather than a link because it changes something, and it
 * opens the `reassign` form in the actions menu rather than carrying its own — see `_owner`. */
/* `border: 0` and not `font: inherit`: a button brings its own border, and inheriting the font
 * would drop the mono face `.owner` above sets — the two chips have to look identical, since one
 * of them is on the index and one is on the run page and they name the same thing. */
.owner-set { display: inline-flex; align-items: center; gap: 5px; border: 0; cursor: pointer; }
.owner-set:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 12%, transparent); }
.owner-set.nobody:hover { border-color: var(--ink-3); }
.owner-pen { display: inline-flex; opacity: 0; transition: opacity 0.12s; }
.owner-set:hover .owner-pen, .owner-set:focus-visible .owner-pen { opacity: 0.7; }

/* Nobody is a finding, not a blank. These are the runs no human is watching, so they are
 * legible rather than hidden — but quieter than a name, because a name is the normal case. */
.owner.nobody { color: var(--ink-3); background: none; border: 1px dashed var(--rule-2); font-style: italic; }

/* Right-aligned on the card so it lines up down a column; inline on the run page, where it
 * sits in a row of metadata that reads left to right. */
.card-flow { display: flex; align-items: center; gap: 8px; }
.card-flow .owner { margin-left: auto; }

/* The run id keeps its lowercase inside .card-flow, which is uppercased. An
   uppercased ADW-20260812-A869 is not the string you would paste anywhere. */
.card-flow .dim { text-transform: none; letter-spacing: 0; }

.gate { margin-bottom: 11px; padding-left: 9px; border-left: 2px solid var(--rule); }
.gate.ok { border-left-color: var(--good); }
.gate.bad { border-left-color: var(--bad); }
.gate-head { display: flex; align-items: baseline; gap: 7px; }
.gate-i { font-size: 11px; }
.gate.ok .gate-i { color: var(--good-text); }
.gate.bad .gate-i { color: var(--bad); }
.gate-n { font-family: var(--mono); font-size: 11.5px; word-break: break-all; }
.checks { list-style: none; margin: 4px 0 0; padding: 0; }
.checks li { display: flex; gap: 6px; font-family: var(--mono); font-size: 11px; color: var(--ink-2); padding: 1px 0; }
.checks li.bad { color: var(--bad); }
.check-i { color: var(--ink-3); }
.checks li.bad .check-i { color: var(--bad); }
.check-k { flex: 0 0 auto; }
.check-n { color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; }
.checks li.bad .check-n { color: var(--bad); }

.prompt > summary { cursor: pointer; display: flex; gap: 9px; align-items: baseline; font-family: var(--mono); font-size: 11.5px; padding: 4px 0; }
.prompt > summary .dim { margin-left: auto; }
.out { margin-bottom: 11px; }
.out-head { display: flex; align-items: baseline; gap: 10px; }
.out-t { font-family: var(--mono); font-size: 11.5px; font-weight: 700; }
.ok-t { color: var(--good-text); font-size: 11px; }
.bad-t { color: var(--bad); font-size: 11px; }
.warn-t { color: var(--serious); font-size: 11px; }

/* A phase's output, in the wide column and in the body font.
   The same weight decision the transcript makes: what the phase concluded gets prose and room,
   mechanism stays small and dim. It was `<pre>` in the metadata rail, which is why nobody read it. */
.out-sum { margin: 7px 0 0; color: var(--ink); line-height: 1.55; max-width: 74ch; }
.out-h { margin: 14px 0 7px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); display: flex; gap: 7px; align-items: baseline; }
.out-l { margin: 0; padding-left: 16px; }
.out-l li { margin: 2px 0; }
.out-p { white-space: pre-wrap; line-height: 1.5; max-width: 74ch; }
/* Screenshots a QA walk left behind, in the metadata rail's kv table.
   Thumbnails rather than full size: the rail is narrow, a 1600x900 desktop shot laid out at its
   own width would blow the column open, and the point of the row is "there are pictures, here
   they are" — each links to itself for the reader who wants to actually look. */
.shot { display: inline-block; margin: 0 4px 4px 0; line-height: 0; }
.shot img { width: 96px; height: auto; border: 1px solid var(--rule); border-radius: 3px; }
.shot:hover img { border-color: var(--ink-3); }

/* The QA report. Screenshots are the evidence here rather than a footnote, so they are shown at a
   size somebody can read a font in — before and after side by side where there is room, stacked
   on a phone. */
.qa-report { max-width: 1100px; margin: 0 auto; padding: 16px; }
.qa-report h2 { margin-top: 28px; font-size: 15px; }
.qa-summary { color: var(--ink-2); max-width: 74ch; }
.qa-note { color: var(--ink-3); font-size: 13px; max-width: 74ch; }
.qa-counts { display: flex; flex-wrap: wrap; gap: 6px; }
.qa-pill { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 12px; border: 1px solid var(--rule); color: var(--ink-2); }
.qa-pill.pass { border-color: var(--good); color: var(--good-text); }
.qa-pill.fail { border-color: var(--bad); color: var(--bad); }
.qa-pill.blocked, .qa-pill.not_attempted, .qa-pill.left { border-color: var(--warn); }
.qa-case { border: 1px solid var(--rule); border-left-width: 3px; border-radius: 4px; padding: 12px; margin: 10px 0; background: var(--surface); }
.qa-case.pass { border-left-color: var(--good); }
.qa-case.fail { border-left-color: var(--bad); }
.qa-case.blocked, .qa-case.not_attempted { border-left-color: var(--warn); }
.qa-case header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.qa-kind, .qa-reason { color: var(--ink-3); font-size: 12px; }
.qa-outcome dt { font-size: 12px; color: var(--ink-3); margin-top: 6px; }
.qa-outcome dd { margin: 0; }
.qa-shots { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }
.qa-shots figure { margin: 0; flex: 1 1 320px; min-width: 0; }
.qa-shots figcaption { font-size: 12px; color: var(--ink-3); margin-bottom: 4px; }
.qa-report .shot img { width: 100%; max-width: 520px; }
/* The rail's kv table stretched to the full width of the wide column, so a one-word value sat
   alone at the far right of the page. Same measure as the prose above it. */
.out .usage.kv { max-width: 74ch; }
.out-raw > summary { cursor: pointer; font-family: var(--mono); font-size: 11px; color: var(--ink-3); padding: 6px 0; }

/* One finding. Severity leads because it is the ranking — critical and important block, minor and
   nit are posted and left to a person's judgement — and the four tokens are the ones the banners
   already use, so this is legible in both themes without a new colour. */
.fi { border-left: 2px solid var(--rule); padding: 3px 0 7px 11px; margin-bottom: 9px; }
.fi-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.fi-s { font-family: var(--mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 6px; border-radius: 4px; }
.fi-s.s-critical { color: var(--bad); background: color-mix(in srgb, var(--bad) 12%, transparent); }
.fi-s.s-important { color: var(--ink); background: color-mix(in srgb, var(--serious) 20%, transparent); }
.fi-s.s-minor { color: var(--ink-2); background: color-mix(in srgb, var(--info) 14%, transparent); }
.fi-s.s-nit { color: var(--ink-3); background: var(--surface-2); }
.fi-loc { font-family: var(--mono); font-size: 11px; color: var(--ink-3); word-break: break-all; }
.fi-k { font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
.fi-sum { margin: 5px 0 0; line-height: 1.5; max-width: 74ch; }
.fi.killed { opacity: 0.55; }
.fi.killed .fi-sum { text-decoration: line-through; }
.fi-d > summary { cursor: pointer; font-family: var(--mono); font-size: 11px; color: var(--ink-3); padding: 5px 0 0; }
.fi-body { white-space: pre-wrap; line-height: 1.5; color: var(--ink-2); margin-top: 4px; max-width: 74ch; }
.bar-go { margin-left: 8px; font-weight: 600; white-space: nowrap; }

pre {
  margin: 5px 0 0;
  padding: 9px 11px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 7px;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.55;
  max-height: 40vh;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.d-right h3 { margin: 12px 0 8px; font-family: var(--mono); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; display: flex; gap: 7px; align-items: baseline; }

/* --- the transcript --------------------------------------------------------
 *
 * A phase read as a log: every row the same 11px monospace, timestamp and event type in
 * fixed columns, the agent's own sentences indistinguishable from `gate.passed`. This is
 * the same events, weighted — what the model said in the body font with room around it,
 * mechanism small and dim underneath.
 *
 * No timestamp column. It cost 60px on every row to answer a question nobody was asking;
 * durations still sit on the rows where time is the point (a tool call, a phase). */
/* No height of its own: the detail sheet's right column is the scroller now, and two
 * nested scrollers is a transcript you have to scroll twice to reach the end of. */
.tx { padding: 4px 0 10px; }

/* What the model said. The only thing here at full contrast, deliberately. */
.tx-say, .tx-think, .tx-human { display: flex; gap: 9px; padding: 11px 14px 12px 12px; }
.tx-say + .tx-say { padding-top: 0; }
.tx-dot { flex: 0 0 12px; font-size: 10px; line-height: 1.7; color: var(--s3); user-select: none; }
.tx-body { flex: 1; min-width: 0; font-size: 13px; line-height: 1.62; color: var(--ink);
           white-space: pre-wrap; overflow-wrap: anywhere; }

.tx-think .tx-dot, .tx-think .tx-body { color: var(--ink-3); }
.tx-think .tx-body { font-style: italic; }

/* A person interrupting a live agent — the one row here somebody else wrote. */
.tx-human { background: var(--s2-wash, rgba(80,140,255,0.07)); border-radius: 6px; margin: 6px 8px; }
.tx-human .tx-dot { color: var(--s2); }
.tx-who { display: block; margin-top: 3px; font-size: 11px; color: var(--ink-3); }

/* A tool call: one tight line, the shape a terminal gives it. Indented to the prose column
 * so the conversation reads down a single left edge. */
.tx-tool { display: flex; align-items: baseline; gap: 8px; padding: 2px 14px 2px 33px;
           font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
.tx-tool-n { flex: 0 0 auto; color: var(--s1); }
.tx-tool-a { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Scaffolding — gates, logs, phase boundaries. Present, findable, quiet. */
.tx-meta { display: flex; align-items: baseline; gap: 9px; padding: 2px 14px 2px 33px;
           font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }
.tx-meta.bad, .tx-meta.bad .tx-k { color: var(--bad); }
.tx-k { flex: 0 0 96px; }
.tx-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tx-t { flex: 0 0 auto; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }

/* The envelope it filed. Collapsed: it is seven thousand characters of JSON, it is the
 * conclusion rather than the conversation, and `outputs` above renders it properly. */
.tx-report { margin: 8px 8px 10px 33px; }
.tx-report > summary { cursor: pointer; display: flex; align-items: baseline; gap: 8px;
                       font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
.tx-report > summary::-webkit-details-marker { display: none; }
.tx-report > pre { margin: 7px 0 0; padding: 10px 12px; background: var(--wash, rgba(0,0,0,0.03));
                   border: 1px solid var(--rule); border-radius: 6px; font-size: 11px;
                   line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 40vh;
                   overflow-y: auto; }

/* --- breaks in the axis ---------------------------------------------------
 *
 * A run contains waiting: a human gate can take days, and drawn to scale that
 * leaves the actual work as a hairline at each end of an empty page. Idle
 * stretches collapse to a fixed narrow band carrying their real duration — the
 * same trick a bar chart uses rather than drawing eight blank ninths of itself.
 *
 * Hatched rather than solid so it reads as "removed", not as "something ran here".
 */
.gap-head {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-left: 1px dashed var(--rule-strong);
  border-right: 1px dashed var(--rule-strong);
}

.gap-t {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  white-space: nowrap;
  /* The band is deliberately narrow, so the label is allowed out of it. */
  transform: translateY(-1px);
}

.gap-band {
  position: absolute;
  top: 0;
  bottom: 0;
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0 3px,
    color-mix(in srgb, var(--ink) 7%, transparent) 3px 4px
  );
  border-left: 1px dashed var(--rule);
  border-right: 1px dashed var(--rule);
  pointer-events: none;
}

/* --- who is signed in ------------------------------------------------------ */

.who { display: flex; align-items: center; gap: 8px; }

.who-face {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  /* A ring rather than a border so it sits on the blurred bar without adding a
     hard edge that competes with the rule below it. */
  box-shadow: 0 0 0 1px var(--rule);
}

.who-name {
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1;
  color: var(--ink-2);
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* button_to renders a form; it must not introduce layout of its own. */
/* A flex item, not an inline box. `display: inline` is blockified inside the flex
   parent anyway, and the button then sits on its own baseline — which put SIGN OUT a
   few pixels below the name beside it. */
.who form, form.button_to { display: flex; align-items: center; margin: 0; }

.who-out {
  line-height: 1;
  border: 0;
  background: transparent;
  padding: 0;
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  cursor: pointer;
}

.who-out:hover { color: var(--ink); text-decoration: underline; }

/* --- what a person can do ---------------------------------------------------
 *
 * `Move.for(run)` in the order `Move::CATALOGUE` puts them, split by weight rather than laid out
 * as a row. A row of six equal outlines is a page with no opinion about what it is asking for,
 * and the catalogue's order already carries that opinion — nothing was reading it.
 *
 * The primary button and a menu row are the same partial with different CSS. That is deliberate:
 * a move must not be able to mean two things depending on where it landed.
 */
.run-actions { flex: none; display: flex; align-items: center; gap: 7px; }
.run-actions form.button_to { display: inline-flex; margin: 0; }

.move-primary {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 15px;
  border: 1px solid var(--ink);
  border-radius: 8px;
  background: var(--ink);
  color: var(--page);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  list-style: none;
}

.move-primary::-webkit-details-marker { display: none; }
.move-primary:hover { background: var(--ink-2); border-color: var(--ink-2); }
.move-primary .move-hint { display: inline-flex; opacity: 0.7; }

/* Destructive, and it says so before it is pressed rather than only in the confirm. `_actions`
 * sorts these last so one can never reach the primary slot, and this is the backstop for the day
 * something else does the sorting. */
.move-primary.kind-danger { background: var(--bad); border-color: var(--bad); color: #fff; }

/* Everything else. `<details>` rather than a scripted dropdown: toggling, focus and the ARIA are
 * the element's own behaviour, and the one thing it lacks — closing on an outside click — is a
 * rule `dashboard.js` already has for the account menu. */
.overflow { position: relative; flex: none; }

.overflow-face {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--rule-2);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  list-style: none;
}

.overflow-face::-webkit-details-marker { display: none; }
.overflow-face:hover, .overflow[open] .overflow-face { background: var(--surface-2); color: var(--ink); }

.overflow-menu {
  position: absolute;
  top: 40px;
  right: 0;
  z-index: 90;
  width: 250px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 10px;
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink) 14%, transparent);
  /* A laptop in a call, a short window, a run offering nine moves and a dozen refusals: the menu
     was taller than the screen and the bottom of it — where Abandon lives — was unreachable.
     ponytail: 120px is the top bar (52) plus the head row above the button (52) plus air. The
     page scrolls under a sticky bar, so as it scrolls this only ever over-reserves. An exact
     figure wants anchor positioning, or JS on toggle. */
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.overflow-menu form.button_to { display: block; margin: 0; }
.overflow-eyebrow { margin: 0; padding: 6px 10px 4px; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); }

/* Why the list is not the same on the next run somebody opens. Without it the menu reads as the
 * app's whole vocabulary, and a reader who saw Continue yesterday concludes something is broken. */
.overflow-foot { margin: 5px 8px 2px; padding-top: 7px; border-top: 1px solid var(--hair); font-size: 11px; color: var(--ink-3); line-height: 1.4; }

/* The refusals, which are prose and not controls — so they are deliberately not `.move-item`.
 * Indented under their own eyebrow and dimmer than anything clickable, because a reader scanning
 * for a button must never mistake one of these for a disabled one. */
.overflow-blocked { margin: 3px 8px 0; padding-top: 6px; border-top: 1px solid var(--hair); }
.overflow-blocked dt { font-size: 12px; color: var(--ink-2); }
.overflow-blocked dd { margin: 0 0 6px; font-size: 11px; color: var(--ink-3); line-height: 1.4; }

.move-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--ink);
  text-align: left;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  text-decoration: none;
  list-style: none;
}

.move-item::-webkit-details-marker { display: none; }
.move-item:hover { background: var(--surface-2); }
.move-item .ic { flex: none; color: var(--ink-3); }
.move-item .move-label { flex: 1; min-width: 0; }

/* What the move will do, under its own name. Dim and small because the label is what somebody is
 * scanning for and this is what they read once they have found it — a sentence at label weight
 * turns a seven-item menu into seven paragraphs shouting equally. `align-items: flex-start` on the
 * row so the icon sits with the label rather than floating beside a two-line block. */
.move-item { align-items: flex-start; }
.move-item .ic { margin-top: 1px; }
.move-item .move-says { display: block; margin-top: 2px; font-size: 11px; font-weight: 400; color: var(--ink-3); line-height: 1.35; white-space: normal; }
.move-item .move-hint { flex: none; display: inline-flex; font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
.move-item.kind-danger { color: var(--bad); }
.move-item.kind-danger .ic { color: var(--bad); }
.move-item.kind-danger:hover { background: color-mix(in srgb, var(--bad) 10%, transparent); }

/* A move that needs something typed opens in place rather than in a modal: the run is what
 * somebody is reading, and a form that covers it to ask one question loses the context of the
 * answer. In the header it drops as a panel; in the menu it opens the row it belongs to. */
.move-form { display: block; }
.move-form > summary { list-style: none; }
.move-form > summary::-webkit-details-marker { display: none; }
.move-form[open] > summary.move-item { background: var(--surface-2); }
.run-actions > .move-form { position: relative; }
.run-actions > .move-form > .move-fields { position: absolute; top: 40px; right: 0; margin: 0; }

/* In the menu it grows the menu, rather than floating a wider panel out of it. The absolute
 * version was `width: min(360px, …)` inside a 250px menu pinned to `right: 0`, so the panel hung
 * off the right edge of the window and its first sentence was unreadable — which is the shape of
 * bug that only ever shows up in a browser. */
.overflow-menu .move-fields {
  position: static;
  width: auto;
  margin: 2px 0 4px;
  padding: 9px 10px 10px;
  border: 0;
  border-radius: 7px;
  background: var(--surface-2);
  box-shadow: none;
}

.move-fields {
  z-index: 20;
  margin-top: 6px;
  width: min(360px, calc(100vw - 48px));
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 9px;
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink) 16%, transparent);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.move-fields label { font-size: 12px; color: var(--ink-2); }
.move-fields p { margin: 0; font-size: 12px; }

.move-fields textarea,
.move-fields input[type="text"],
.move-fields input[type="number"] {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 7px 9px;
  resize: vertical;
  width: 100%;
  box-sizing: border-box;
}

.move-fields textarea:focus,
.move-fields input:focus { outline: 2px solid var(--ink-3); outline-offset: -1px; }

.move-go {
  align-self: flex-start;
  height: 30px;
  padding: 0 16px;
  border: 1px solid var(--ink);
  border-radius: 8px;
  background: var(--ink);
  color: var(--page);
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  cursor: pointer;
}

.move-go:hover { background: var(--ink-2); border-color: var(--ink-2); }

/* --- the account menu -------------------------------------------------------
 *
 * Three controls had accumulated in the header — theme, time zone, sign out — and
 * two of them are settings rather than actions, which is where a row stops working
 * and a menu starts. Built on <details>, so open/close, focus, Escape and the ARIA
 * come from the browser.
 */

.account { position: relative; }

.account-face {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  list-style: none;
  padding: 3px 8px 3px 3px;
  border-radius: 999px;
  border: 1px solid transparent;
}

/* Safari still draws a triangle without this, and it is not in the design. */
.account-face::-webkit-details-marker { display: none; }

.account-face:hover,
.account[open] .account-face { border-color: var(--rule); background: var(--surface-2); }

.account-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 40;
  min-width: 260px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 10px;
  /* The header is translucent and blurred; the menu must not be, or the page shows
     through the thing you are reading. */
  box-shadow: 0 12px 28px rgb(0 0 0 / 0.18);
}

.account-who { padding: 10px 12px 8px; border-bottom: 1px solid var(--rule); margin-bottom: 4px; }

.account-email {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

.account-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0;
  padding: 8px 12px;
}

.account-label {
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.account-select {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 4px 6px;
  max-width: 150px;
}

/* One rule for both, rather than two that differ by a property nobody notices until
   the link and the button sit next to each other and do not line up. */
.account-link,
.account-out {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border-radius: 7px;
  border: 0;
  background: transparent;
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  text-decoration: none;
  cursor: pointer;
}

.account-tools { padding-bottom: 2px; border-top: 1px solid var(--rule); margin-top: 4px; padding-top: 10px; }

.account-link:hover,
.account-out:hover { background: var(--surface-2); color: var(--ink); }

/* The sign-out form is a flex item like the rest of the menu, not a row of its own. */
.account form.button_to { display: block; margin: 0; border-top: 1px solid var(--rule); margin-top: 4px; padding-top: 4px; }

/* --- your API keys --------------------------------------------------------- */

/* Full width, for the reason `.people` is: five columns, one of them a masked key, and the
   revoke button was being pushed onto its own line at 46rem while two thirds of the screen sat
   empty. The prose keeps its own cap below — a paragraph the width of a 1600px screen is
   unreadable in the opposite direction. */
.keys { padding: 26px 26px 80px; overflow-x: auto; }
.keys h1 { font-size: 22px; letter-spacing: -0.02em; margin: 0 0 10px; }
.keys-lede { color: var(--ink-2); max-width: 40rem; margin: 0 0 10px; }
.keys-link { text-decoration: underline; text-decoration-color: var(--rule-2); }

/* The one-time reveal. Loud on purpose: it is the only moment this value exists. */
.keys-minted {
  max-width: 40rem;
  border: 1px solid var(--good);
  background: color-mix(in srgb, var(--good) 7%, transparent);
  border-radius: 10px;
  padding: 14px 16px;
  margin: 18px 0;
}
.keys-minted-head {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--good-text);
  margin-bottom: 8px;
}
.keys-token {
  font-family: var(--mono);
  font-size: 13px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 7px;
  padding: 11px 13px;
  margin: 0 0 8px;
  overflow-x: auto;
  /* Selectable in one gesture — this gets copied, always. */
  user-select: all;
}

.keys-form { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin: 22px 0 26px; }
.keys-label {
  display: block;
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  margin-bottom: 5px;
}
.keys-input {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 7px;
  padding: 7px 10px;
  min-width: 17rem;
}
.keys-btn, .keys-revoke {
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 7px 14px;
  cursor: pointer;
}
.keys-btn:hover, .keys-revoke:hover { color: var(--ink); border-color: var(--ink-3); background: var(--surface-2); }
.keys-revoke { padding: 3px 10px; }
.keys-form form.button_to, .keys-table form.button_to { display: inline-flex; margin: 0; }

/* Both pages that use this fill their width, so the table does too. `.dev-table` is
   `display: block` with `overflow-x: auto`, which is right for the narrow documentation tables it
   was written for — but a block table's `width: 100%` sizes the *block* and leaves the table inside
   it at content width, so lifting the page's cap on its own left the columns huddled in the left
   third of a wide screen. `display: table` makes the width apply to the table itself, and the
   horizontal scroll moves up to `.keys` / `.people` so a narrow window still scrolls the table
   rather than the page body.

   The `table` in the selector is the whole rule. Both classes are on the same element and both are
   one class of specificity, so the cascade is decided by file order — and `.dev-table`'s
   `display: block` is 150 lines *below* this, which is why unscoping this from `.people` silently
   put both pages back to content width while reading as a tidy-up. The element qualifier settles
   it wherever either rule moves to. Measured rather than read: the last column of the registry
   ended at 1236px on a 2187px window before, and 2146px after. */
table.keys-table { display: table; width: 100%; margin-top: 8px; }
.keys-revoked td { color: var(--ink-3); }

/* --- the team registry ----------------------------------------------------- */

/* Everything on this page reuses the `.keys-*` form primitives above — they are the only ones the
   app has, and they keep the name of the page that introduced them rather than being renamed across
   two views for tidiness. */
/* Full width on purpose. Eight columns of handles, and a Linear user id is 36 characters — at
   72rem every id wrapped to three lines and the table was mostly whitespace and hyphens. The
   prose is *not* freed with it: `.keys-lede` keeps its own 40rem, because a paragraph running the
   width of a 1600px screen is unreadable in the opposite direction. */
.people { padding: 26px 26px 80px; overflow-x: auto; }
.people h1 { font-size: 22px; letter-spacing: -0.02em; margin: 0 0 10px; }

/* A handle we do not have. Dimmer than `.dim`, because "no Slack id" is a fact about three
   teammates rather than a gap somebody should go and fill. */
.people-none { color: var(--ink-3); }

/* A handle resolved to a name, with the stored id under it. The name leads because it is the half
   a person can read; the id stays because it is the half the table stores and the form edits. */
.people-handle { line-height: 1.35; }
.people-id { color: var(--ink-3); font-size: 11px; line-height: 1.35; }

/* A well-formed id that the system it belongs to has never heard of. Rendered only when the
   directory came back whole — see `_handle.html.haml` for why that condition is the whole point. */
.people-wrong { color: var(--bad); font-size: 11px; line-height: 1.35; }

/* Why a column is showing raw ids. Nearly always a missing scope, which is a sentence somebody can
   act on rather than an empty cell they have to interpret. */
.people-lookup { display: grid; gap: 4px; margin: 14px 0 0; }

/* Somebody who has signed in and is not in the registry. `bin/gary doctor` already reports this as
   informational; here it is next to the button that fixes it. */
.people-strangers { margin-top: 34px; border-top: 1px solid var(--rule); padding-top: 18px; }

/* Stacked rather than the inline `.keys-form`, which is right for one field and cramped for five.
   Labels, inputs and buttons are still the shared primitives. */
.people-form { display: grid; gap: 16px; max-width: 32rem; margin: 22px 0 26px; }
.people-flags { display: grid; gap: 10px; border-top: 1px solid var(--rule); padding-top: 16px; }
.people-flag { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.people-actions { display: flex; gap: 12px; align-items: center; }
/* `.keys-table form.button_to` already displays the form inline; this only stops the column
   collapsing to the width of the word "On". */
.people-toggle { white-space: nowrap; }

/* --- API documentation -----------------------------------------------------
 *
 * A reference, so it is laid out to be scanned and jumped around rather than read: a
 * sticky index on the left, one card per endpoint on the right. Same tokens as the rest of
 * the dashboard — this is Gary's page, not a copy of another application's.
 */

.dev {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: 34px;
  padding: 22px 26px 80px;
  align-items: start;
}

.dev-nav {
  position: sticky;
  top: 72px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
}

.dev-nav-title { font-weight: 650; letter-spacing: -0.01em; }
.dev-ver {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: 12px;
}

.dev-nav-group {
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-3);
  margin: 16px 0 4px;
}

.dev-link {
  display: flex;
  align-items: baseline;
  gap: 7px;
  padding: 5px 8px;
  border-radius: 6px;
  font-size: 12.5px;
  color: var(--ink-2);
  text-decoration: none;
}
.dev-link:hover { background: var(--surface-2); color: var(--ink); }

.dev-body { min-width: 0; max-width: 52rem; }
.dev-section { padding-bottom: 34px; margin-bottom: 34px; border-bottom: 1px solid var(--rule); }
.dev-section:last-child { border-bottom: 0; }

.dev-section h1 { font-size: 26px; letter-spacing: -0.02em; margin: 0 0 10px; }
.dev-section h2 { font-size: 18px; letter-spacing: -0.01em; margin: 0 0 8px; }
.dev-section h3 { font-size: 13.5px; margin: 20px 0 6px; }
.dev-section h4 {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  margin: 18px 0 6px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.dev-section p { margin: 0 0 10px; max-width: 44rem; line-height: 1.62; color: var(--ink-2); }
.dev-lede { font-size: 15px; }

.dev-base, .dev-kv { display: flex; align-items: baseline; gap: 10px; margin-top: 8px; }
.dev-k {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  min-width: 74px;
}

.dev-note { border-left: 2px solid var(--rule-2); padding-left: 13px; margin: 14px 0; }
.dev-note strong { font-size: 13px; }
.dev-note p { margin: 4px 0 8px; }

.dev-code {
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.6;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 11px 13px;
  overflow-x: auto;
  margin: 0 0 10px;
  color: var(--ink);
}

.dev-table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin: 6px 0 12px; display: block; overflow-x: auto; }
.dev-table th {
  text-align: left;
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  font-weight: 400;
  padding: 6px 10px 6px 0;
  border-bottom: 1px solid var(--rule);
}
.dev-table td { padding: 7px 10px 7px 0; border-bottom: 1px solid var(--rule); vertical-align: top; color: var(--ink-2); }
.dev-table tr:last-child td { border-bottom: 0; }
.dev-params td:first-child { color: var(--ink); white-space: nowrap; }

.req {
  font-family: var(--mono);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--serious);
  border: 1px solid color-mix(in srgb, var(--serious) 40%, transparent);
  border-radius: 999px;
  padding: 1px 6px;
  white-space: nowrap;
}
.req.auth { color: var(--ink-3); border-color: var(--rule); }

.status {
  font-family: var(--mono);
  font-size: 11px;
  border-radius: 4px;
  padding: 1px 6px;
  font-variant-numeric: tabular-nums;
}
.status.ok { color: var(--good-text); background: color-mix(in srgb, var(--good) 12%, transparent); }
.status.bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }

.verb {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  border-radius: 3px;
  padding: 2px 5px;
  white-space: nowrap;
}
.verb.get { color: var(--info); background: color-mix(in srgb, var(--info) 12%, transparent); }
.verb.post { color: var(--good-text); background: color-mix(in srgb, var(--good) 14%, transparent); }

.endpoint {
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 16px 18px;
  margin: 16px 0;
  background: var(--surface);
  /* An anchored jump lands under the sticky header without this. */
  scroll-margin-top: 78px;
}

.endpoint-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.endpoint-path { font-family: var(--mono); font-size: 12.5px; color: var(--ink); }
.endpoint-summary { margin: 10px 0 4px; font-size: 14px; }

@media (max-width: 860px) {
  .dev { grid-template-columns: minmax(0, 1fr); }
  .dev-nav { position: static; max-height: none; }
}

/* --- the sign-in gate ------------------------------------------------------
 *
 * The one page an unauthenticated stranger can see, so it says what this is and
 * who it is for and nothing else. No navigation: there is nowhere to go.
 */

/* `signin`, not `gate`: run detail owns `.gate` for a gate result, and both rules
   matched this page — the card was inheriting a stray left border and padding from
   a component it has nothing to do with. */
.signin {
  min-height: 78vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 26px;
}

.signin-card {
  width: 100%;
  max-width: 380px;
  padding: 30px 32px 30px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 14px;
  text-align: center;
}

/* Gary himself. `height: auto` with the intrinsic size on the tag, so the card does
   not jump when he lands. */
.signin-mark {
  display: block;
  width: 150px;
  height: auto;
  margin: 0 auto 2px;
  /* He is standing on the card, so give him something to stand on. */
  filter: drop-shadow(0 5px 9px rgba(11, 11, 11, 0.2));
}

.signin-card h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; }
.signin-no { color: var(--bad); font-size: 20px; }
.signin-sub { margin: 8px 0 0; font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }

.signin-btn {
  margin-top: 22px;
  width: 100%;
  padding: 10px 16px;
  border: 1px solid var(--rule);
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}

.signin-btn:hover { border-color: var(--rule-2); background: var(--surface); }

.signin-note { margin: 14px 0 0; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }

/* --- the conversation -------------------------------------------------------
 *
 * One drawer, not two blocks. The thread used to render under the waterfall and the box to type
 * in under that, so on a run with thirty phases the two halves of one conversation were a scroll
 * apart and neither was visible while you read the chart.
 *
 * The thread is inside the polled region and the composer is not — see `_composer` — so they are
 * siblings in the DOM and land in one column by grid placement. That is the layout being arranged
 * around a correctness constraint rather than the constraint being worked around.
 */
.drawer {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: color-mix(in srgb, var(--page) 55%, var(--surface));
  border-left: 1px solid var(--rule);
}

.drawer-head {
  flex: none;
  height: 44px;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 8px 0 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  color: var(--ink-2);
}

.drawer-title { font-family: var(--mono); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink); }
.drawer-count { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }

.drawer-shut {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
}

.drawer-shut:hover { background: var(--surface-2); color: var(--ink); }

.thread { flex: 1; min-height: 0; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 16px; }

/* A run drawer's two tabs. The active one reads as the title; the other is quieter, and the
   composer belongs to the conversation, so it goes away with it and the activity takes its row. */
.drawer-tab {
  display: inline-flex; align-items: center; gap: 7px; height: 30px; padding: 0 8px;
  border: 0; border-radius: 7px; background: transparent; color: var(--ink-3); cursor: pointer;
}
.drawer-tab .drawer-title { color: inherit; }
.drawer-tab:hover { background: var(--surface-2); color: var(--ink); }
.drawer-tab-chat,
[data-drawer-tab="activity"] .drawer-tab-act { color: var(--ink); background: var(--surface-2); }
[data-drawer-tab="activity"] .drawer-tab-chat { color: var(--ink-3); background: transparent; }
.drawer .act-pane { display: none; }
[data-drawer-tab="activity"] .drawer .thread:not(.act-pane) { display: none; }
[data-drawer-tab="activity"] .drawer .act-pane { display: flex; }
[data-drawer-tab="activity"] body.run-page .composer { display: none; }
[data-drawer-tab="activity"] .drawer { grid-area: 2 / 2 / 4 / 3; }
.act-run { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); text-decoration: none; }
.activity-page { padding: 26px 26px 80px; max-width: 880px; }
.activity-day { margin: 26px 0 0; font-size: 14px; }
.activity-feed { overflow: visible; padding: 14px 0; }
.activity-older { display: inline-block; margin-top: 12px; text-decoration: none; }
.act-run:hover { color: var(--ink); text-decoration: underline; }
.thread-empty { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-3); font-style: italic; }

.turn-who { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; font-family: var(--mono); font-size: 11.5px; }
.turn-name { font-weight: 700; color: var(--ink); }
.turn-human .turn-name { color: var(--s1); }
.turn-at { font-size: 10px; color: var(--ink-3); }

.turn-text { padding: 11px 13px; border-radius: 10px; font-size: 12.5px; line-height: 1.55; white-space: pre-wrap; text-wrap: pretty; }
.turn-human .turn-text { background: color-mix(in srgb, var(--s1) 8%, var(--surface)); border: 1px solid color-mix(in srgb, var(--s1) 22%, var(--surface)); }
.turn-gary .turn-text { background: var(--surface); border: 1px solid var(--rule); }

/* Gary is on it. No bubble and no border, because there is nothing in it yet: a dashed box the
 * size of a paragraph reads as a message that has arrived and says something worrying, which is
 * exactly how the placeholder that used to live here was read. The dots are the message. */
.turn-pending .turn-name { color: var(--ink-3); }

/* The one animation on this page, and it earns it: everything else here moves because the poll
 * swapped it, and a reader cannot tell that apart from a page that has stopped. Three dots are
 * what every chat uses for the same second of waiting, so it needs no explaining — which was the
 * whole problem with the sixty words it replaced. */
.turn-dots { display: flex; align-items: center; gap: 4px; height: 22px; padding-left: 2px; }
.turn-dots span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--ink-3);
  animation: turn-dot 1.3s ease-in-out infinite;
}
.turn-dots span:nth-child(2) { animation-delay: 0.16s; }
.turn-dots span:nth-child(3) { animation-delay: 0.32s; }

@keyframes turn-dot {
  0%, 65%, 100% { opacity: 0.22; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-2px); }
}

/* Somebody who has asked not to be moved at still has to be able to tell that Gary is on it. */
@media (prefers-reduced-motion: reduce) {
  .turn-dots span { animation: none; opacity: 0.45; }
}

/* What it is saying while it works. Quieter than the placeholder above it and quieter still than
 * an answer, because it is neither: a sentence the agent said on the way to one, which the next
 * poll may replace with a different sentence. Not italic — the placeholder is, and these two have
 * to be tellable apart at a glance. `pre-wrap` because it is prose the model wrote. */
.turn-aloud {
  margin-top: 7px;
  padding: 0 13px;
  border-left: 2px solid var(--rule);
  margin-left: 2px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-3);
  white-space: pre-wrap;
  text-wrap: pretty;
}

/* A steer overrode the agent's own plan and a `go` authorised a pipeline. Neither is a question,
 * and a reader scanning the thread should not have to infer which is which from the prose. */
.turn-kind {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0 6px;
}

/* The drawer put away. The count is the whole reason this is a button and not an icon: a run
 * somebody asked a question on and walked away from has to be able to say so from here. */
.chat-open {
  display: none;
  align-items: center;
  gap: 7px;
  height: 30px;
  padding: 0 11px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 11.5px;
  cursor: pointer;
  white-space: nowrap;
}

.chat-open:hover { background: var(--surface-2); color: var(--ink); }
[data-chat="shut"] .chat-open { display: inline-flex; }

.chat-count {
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--ink);
  color: var(--page);
  font-size: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.chat-count:empty { display: none; }

/* --- the composer ----------------------------------------------------------- */
.composer { background: var(--surface); border-top: 1px solid var(--rule); border-left: 1px solid var(--rule); padding: 12px; }
.composer-form { display: flex; flex-direction: column; gap: 8px; }

.composer-box {
  font: inherit;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule-2);
  border-radius: 9px;
  padding: 10px 12px;
  height: 74px;
  resize: none;
  width: 100%;
  box-sizing: border-box;
}

.composer-box::placeholder { color: var(--ink-3); }
.composer-box:focus { outline: none; border-color: var(--ink); }

.composer-foot {
  display: flex;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
  font-size: 12px;
}

.composer-foot .dim { font-size: 10.5px; line-height: 1.45; }
.composer-note { flex-basis: 100%; }

/* --- reporting -------------------------------------------------------------
   `/admin/reports`. Almost nothing here is new: the container is `.people`'s,
   the numbers are the dashboard's `.board`/`.metric`, the tables are
   `.dev-table.keys-table`, and the period and person pickers are the index's
   own `.ranges` and `.scopes`. What is left is a bar, a day column, and the
   two ways this page writes a number that does not exist. Tokens only. */

.reports { padding: 26px 26px 80px; overflow-x: auto; }
.reports h1 { font-size: 22px; letter-spacing: -0.02em; margin: 0 0 10px; }

.rep-section { margin-top: 34px; border-top: 1px solid var(--rule); padding-top: 18px; }
.rep-h2 { font-size: 16px; margin: 0 0 8px; letter-spacing: -0.01em; }
.rep-h { font-size: 13px; margin: 0 0 6px; }

/* Prose that qualifies a number. Capped like `.keys-lede` — a caveat nobody
   finishes reading is a caveat that did not ship. */
.rep-note {
  color: var(--ink-2);
  font-size: 12px;
  line-height: 1.5;
  max-width: 46rem;
  margin: 6px 0 0;
}
.rep-note.rep-warn {
  border-left: 2px solid var(--warn);
  padding-left: 10px;
}

/* The two halves of the honesty rule, and they must not look alike. A figure
   is a figure; a reason is prose, because it is a sentence rather than a
   measurement and reading it as one is the failure this page exists to avoid. */
.rep-figure { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.rep-none { color: var(--ink-3); font-style: italic; font-size: 12px; }
.rep-dash { color: var(--ink-3); }

.rep-lead { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; margin: 4px 0 8px; }
.rep-lead .dim { font-size: 12px; font-weight: 400; display: block; margin-top: 3px; }

.rep-empty { color: var(--ink-3); font-style: italic; font-size: 12.5px; margin: 8px 0; }
.rep-nothing { padding: 40px 0; text-align: center; }

/* --- filters --- */

.rep-filters {
  display: flex;
  flex-direction: column;
  gap: 9px;
  margin: 14px 0 4px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--rule);
}
.rep-filter-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* `.ranges` carries `margin-left: auto` so the dashboard can push it to the right of its
   control bar. Here it sits in a labelled row and that auto margin threw the chips to the
   far edge, with the label stranded on its own. Overridden rather than the shared rule
   changed, because the dashboard's placement is correct for the dashboard. */
.rep-filter-row .ranges { margin-left: 0; margin-bottom: 0; }
.rep-filter-k {
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  min-width: 52px;
}
.rep-count { color: var(--ink-3); margin-left: 5px; font-variant-numeric: tabular-nums; }
.rep-dates { display: flex; align-items: center; gap: 6px; }
.rep-date { width: auto; padding: 5px 8px; font-size: 12px; }
.rep-to { color: var(--ink-3); font-size: 11px; }

.rep-caveats { padding: 10px 20px 0; }

/* --- breakdowns --- */

.rep-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: 22px 30px;
  margin-top: 12px;
}
.rep-block { min-width: 0; }
.rep-table tfoot td {
  border-top: 1px solid var(--rule-2);
  border-bottom: 0;
  color: var(--ink-3);
  font-size: 11px;
}

/* Scaled to the largest row rather than to the total, because these tables are
   read to find the big one. The number beside it is absolute, so the choice of
   scale hides nothing. */
.rep-barcol { width: 90px; }
.rep-bar {
  height: 7px;
  min-width: 1px;
  border-radius: 3px;
  background: var(--s1);
}

/* --- the day series --- */

.rep-days {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 120px;
  margin: 14px 0 4px;
  padding-bottom: 16px;
  overflow-x: auto;
}
.rep-day {
  flex: 1 0 14px;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  position: relative;
}
/* A day that cost nothing still draws a hairline, so an empty day and a day
   off the end of the axis do not look the same. */
.rep-day-bar {
  background: var(--s1);
  border-radius: 2px 2px 0 0;
  min-height: 1px;
}
.rep-day-k {
  position: absolute;
  bottom: -15px;
  left: 0;
  right: 0;
  text-align: center;
  font-family: var(--mono);
  font-size: 9px;
  color: var(--ink-3);
}

/* --- what is waiting --- */

.rep-gates { display: flex; gap: 26px; margin: 12px 0 14px; }
.rep-gate-n { font-size: 24px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; }
.rep-gate-k {
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}

.rep-up { color: var(--bad); }
.rep-down { color: var(--good-text); }
/* The one metric on the strip that ignores the filters, saying so where a reader will see it
   before drawing a conclusion — grey rather than the "act now" colour, because it is a
   qualification and not an alarm. */
.rep-unfiltered { color: var(--ink-3) !important; }

/* --- a run's own desktop ---
   Pinned to the bottom of the left column, collapsed to a 38px bar by default.

   The old panel was an always-on 34vh black rectangle on every run with a rig — a third of the
   column, whether or not anybody was watching. What is here now is a status bar that still says
   what the desktop is doing, and a drawer behind one click.

   **Every colour is a token.** The design this implements specifies literal hexes for a light
   page; this app has a dark theme, and a hex here would be correct in one of the two. The dark
   surfaces are the exception and are deliberately literal: the stream and the overlay behind it
   are a screenshot of somebody's actual desktop, and a "dark surface" that followed the theme
   would be a light frame around a dark image in light mode. Those are the same values the
   design gives, because the design was already reasoning about a screen and not about a page. */

.rig { min-width: 0; }

/* --- the collapsed bar ---
   A `<button>` and not a div with a role: Enter and Space then work because that is what a
   button does, rather than because a keydown handler was remembered. */
.rig-bar {
  width: 100%;
  height: 38px;
  padding: 0 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--surface-2);
  /* The top edge only. A full border and a radius made this a card floating above the bottom of
     the page; it is the pane's own bottom edge, which is what the design drew. */
  border: 0;
  border-top: 1px solid var(--rule);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  user-select: none;
}
.rig-bar:hover { background: var(--surface); }
.rig-bar-l { display: flex; align-items: center; gap: 10px; min-width: 0; }
.rig-bar-r {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-3);
}
.rig-glyph { color: var(--ink-3); flex: none; }

.rig-lab {
  flex: none;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* The session and its lease. Last in the row and the only thing allowed to shrink, because the
   state is what a glance is for and the rig id is what a second look is for. */
.rig-sess {
  min-width: 0;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- the state chip ---
   One element, one dot, and the colour comes from the `desk-*` class on the panel — so the chip
   in the bar, the chip in the drawer header and the chip on the overlay cannot disagree about
   what state this desktop is in. `--desk-dot` is the whole mechanism. */
.rig-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--surface);
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-2);
  white-space: nowrap;
}
.rig-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--desk-dot, var(--ink-3));
  flex: none;
}

/* The four states this system can actually tell apart. `human` and `test` from the design are
   absent because no control hand-off exists to derive them from — see `desk_state`. */
.desk-provisioning { --desk-dot: var(--warn); }
.desk-ready { --desk-dot: var(--good); }
.desk-agent { --desk-dot: var(--info); }
.desk-expired { --desk-dot: var(--ink-3); }
.desk-error { --desk-dot: var(--bad); }

/* Only where something is genuinely in flight. A pulsing dot on a released desktop would be the
   page implying work where there is none. */
.desk-provisioning .rig-dot,
.desk-agent .rig-dot { animation: deskPulse 1.6s ease-in-out infinite; }

@keyframes deskPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

/* Somebody who has asked for no motion has asked for no motion. The dot's colour still carries
   the state, so nothing is lost by holding it still. */
@media (prefers-reduced-motion: reduce) {
  .rig-dot, .rig-sweep i { animation: none !important; }
}

/* --- open and shut ---
   Applied to the root before first paint by the layout's head script, the same way the theme,
   the density and the conversation drawer are — so the page does not lay itself out closed and
   then jump. Default is shut for somebody who has never opened it, which is the design's rule
   and the reason the preference is stored as "open" rather than "shut". */
.rig-drawer { display: none; }
[data-desk="open"] .rig-bar { display: none; }
[data-desk="open"] .rig-drawer {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: var(--desk, 320px);
  position: relative;
  padding-top: 8px;
  background: var(--surface-2);
  /* Flush for the same reason the bar is, and it has to be the *same* reason: opening the panel
     must not move it. A card-shaped drawer under a flush bar would jump 14px inwards on every
     click, which reads as the page reflowing rather than as a drawer opening. */
  border: 0;
  border-top: 1px solid var(--rule);
}

.rig-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 12px 8px;
}
.rig-head-l { display: flex; align-items: center; gap: 10px; min-width: 0; }
.rig-head-r { display: flex; align-items: center; gap: 6px; flex: none; }

/* What the desktop is doing, in words. Ellipsized rather than wrapped: the header is one row and
   a second line of it would push the stream down every time an agent's phase name got long. */
.rig-line {
  min-width: 0;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rig-body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0 12px 12px; }

/* --- the buttons ---
   `.rig-btn` is the panel's own small control, and `.rig-go` is the one that ends the interaction
   — `hide` inline, `exit full size` on the overlay.

   **`.rig-release`, not `.rig-release button`.** `button_to`'s `class:` lands on the `<button>`
   itself and its `form:` option is what dresses the wrapping form — so the descendant selector
   matched nothing at all and `release` rendered as a browser-default button, white and unstyled,
   on both surfaces. Inherited from the panel this replaces, where it had the same shape and was
   never noticed: one button among five, and the eye reads "a button" long before it reads "the
   wrong button". Found by asking the page for its computed background, not by reading the file. */
.rig-btn,
.rig-release {
  padding: 5px 11px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
}
.rig-btn:hover,
.rig-release:hover { background: var(--surface-2); color: var(--ink); }
.rig-release:hover { color: var(--bad); border-color: var(--bad); }
.rig-btn[disabled] { color: var(--ink-3); opacity: 0.75; cursor: default; }

.rig-go {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}
.rig-go:hover { background: var(--ink-2); border-color: var(--ink-2); color: var(--page); }

/* --- the stream ---
   Literal darks, per the note at the top of this block: this frame holds a picture of a desktop,
   and it letterboxes itself inside whatever it is given. */
.rig-frame {
  position: relative;
  flex: 1;
  min-height: 0;
  background: #0f0f0e;
  border: 1px solid #23231f;
  border-radius: 4px;
  overflow: hidden;
  display: flex;
}
.rig-view { flex: 1; width: 100%; border: 0; background: #0f0f0e; }

/* Who is driving, over the stream. The design makes this required where the animated cursor is
   optional, and that is the right call: it is the only thing on the frame that says whether what
   you are watching will fight you for the mouse. */
.rig-badge {
  position: absolute;
  left: 10px;
  bottom: 10px;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgb(15 15 14 / 78%);
  border: 1px solid rgb(255 255 255 / 14%);
  font-family: var(--mono);
  font-size: 10.5px;
  color: #dcdbd3;
}

/* --- provisioning ---
   A sweep rather than a spinner, because the wait has a shape: an image to pull and a task to
   schedule, and roughly a minute of it. A spinner says "something is happening"; this says
   "something is happening and it is not stuck". */
.rig-wait {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: repeating-linear-gradient(135deg, #141412 0 10px, #101010 10px 20px);
}
.rig-sweep {
  width: 220px;
  height: 3px;
  border-radius: 2px;
  background: rgb(255 255 255 / 10%);
  overflow: hidden;
}
.rig-sweep i {
  display: block;
  width: 30%;
  height: 100%;
  border-radius: 2px;
  background: var(--warn);
  animation: deskSweep 1.4s linear infinite;
}
@keyframes deskSweep {
  from { transform: translateX(-110%); }
  to { transform: translateX(340%); }
}
.rig-cap { font-family: var(--mono); font-size: 11.5px; color: #c9c8bf; }

/* --- released, or never came up ---
   One glyph, one line. The line is the rig's own words where it has any, because the container
   and its log group are gone by the time anybody looks — the whole argument for `rigs.failure`
   being a column. */
.rig-blank {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 20px;
  text-align: center;
}
.rig-glyph-big { color: #55544d; flex: none; }
.desk-error .rig-glyph-big { color: var(--bad); }
.rig-why { font-family: var(--mono); font-size: 11.5px; color: #a9a8a0; max-width: 64ch; }

/* The recovery button sits *inside* the dark frame, so it takes the on-dark treatment rather than
   the panel's. Same reason the overlay's buttons do: the surface it is on is a picture of a
   screen, and a light button there is a hole in it. */
.rig-again {
  background: rgb(255 255 255 / 8%);
  border-color: rgb(255 255 255 / 16%);
  color: #e9e8e2;
}
.rig-again:hover { background: rgb(255 255 255 / 14%); color: #fff; }

/* --- the full-size overlay ---
   Its own frame rather than the drawer's, because an iframe cannot be moved. `dashboard.js`
   launches it on open and replaces the node on close, so a shut overlay holds no viewer session
   and streams nothing. */
.rig-over { display: none; }
[data-desk-full] .rig-over {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  background: #0b0b0a;
}
.rig-over-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* On dark, so the header reads against the overlay rather than against the page it covers. */
.rig-over .rig-lab { color: #8b8a82; }
.rig-over .rig-sess { color: #77766d; }
.rig-over .rig-chip { background: rgb(255 255 255 / 7%); border-color: rgb(255 255 255 / 16%); color: #e2e1d9; }
.rig-over .rig-btn,
.rig-over .rig-release {
  background: rgb(255 255 255 / 8%);
  border-color: rgb(255 255 255 / 16%);
  color: #e9e8e2;
}
.rig-over .rig-btn:hover,
.rig-over .rig-release:hover { background: rgb(255 255 255 / 14%); color: #fff; }
.rig-over .rig-go { background: #f4f3ee; border-color: #f4f3ee; color: #17170f; }
.rig-over .rig-go:hover { background: #fff; border-color: #fff; color: #17170f; }

/* Where the credentials land once fetched. Hidden until then, because an empty box under a
   button reads as a button that half worked. On the overlay it lands on the header rather than
   the panel, since the overlay is a sibling of `.rig` and not inside it. */
.rig-shown {
  padding: 8px 11px;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-2);
  word-break: break-all;
}
.rig-over .rig-shown { border: 0; color: #c9c8bf; }
