/*
 * The agent console.
 *
 * Everything here is written against the variables at the top of styles.css,
 * which is also where the shared furniture lives - the titlebar, the buttons,
 * the modal. This file is only what this page adds, so a theme is still one
 * block in one file rather than two that have to be kept in step.
 *
 * The layout is a column of pages and the page. The column is always there,
 * at every width: on a desktop it stands beside the page, on a phone it is
 * a strip across the top, and either way there is nothing to open or shut.
 * The page is a list of what it holds, or one thing from that list with a
 * way back - so a phone shows the same thing a desktop does, one column
 * narrower, rather than choosing between two panes.
 */

.console-page {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 13px;
  overflow: hidden;
}

.console-titlebar-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.console-titlebar-actions .ghost-btn {
  padding: 3px 9px;
  font-size: 12px;
}

/* ----------------------------------------------------------------- demo */

/* The band across the top of the demo workspace, for somebody reading it
   without an account. It is part of the console's column, above the pages
   and the page, so it is on screen wherever they navigate to and no page
   has to know about it. Amber rather than green: it is a note about what
   is being looked at, not a piece of the product being looked at. */

.demo-band {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--warn-border);
  background: var(--warn-bg);
  color: var(--warn-text);
}

.demo-band-text {
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
}

.demo-band-name { font-weight: 600; }

.demo-band-btn {
  margin-left: auto;
  flex: none;
  white-space: nowrap;
}

.console-signin-btn {
  padding: 3px 10px;
  font-size: 12px;
  text-decoration: none;
}

/* ------------------------------------------------------------ signed out */

/* One page that scrolls, under a titlebar that does not. The section is
   the scroller, not the document: the page is a column fixed to the
   viewport (`.console-page` above), so the gate takes the rest of it and
   scrolls inside, the way the signed-in console's pages do.

   The sky is behind the whole of it, and it does not move. Everything on
   it does: the pitch and the way in at the top, then the three things the
   product does, every other one of them a white band that passes over the
   stars and covers them, and at the foot the way in again on the bare
   sky. */
.console-gate {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.console-gate[hidden] {
  display: none;
}

/* The stars, once, behind the whole landing.
   They were a band's background and scrolled away with it. Here the picture
   belongs to the scroller itself, and an element that scrolls its content
   paints its own background against its own box - so the sky stays where it
   is while the page travels over it, with no fixed layer to position and
   nothing for a phone's compositor to fight. The picture is a 3:2 night
   sky; `cover` lets the box be any shape and crops the sky rather than
   stretching the stars. The ground colour is the sky's own, so nothing
   flashes light before the file arrives.

   A class rather than the bare element, because the two public pages that
   are not the landing carry the sky's colour and no photograph - see
   `.gate-sky-plain`; console.js turns it on for the landing and off for
   them. */
.console-gate.gate-starfield {
  background: var(--sky) url("sky.jpg") center / cover no-repeat;
}

/* The pitch and the form: no ground of its own now, so what is behind it is
   the stars. */
.gate-sky {
  color: var(--sky-text);
  padding: 88px 0 64px;
}

.gate-top {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 24px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 48px;
  align-items: center;
}

.gate-intro {
  max-width: 640px;
}

.console-gate h1 {
  margin: 0 0 16px;
  font-size: 40px;
  font-weight: 650;
  line-height: 1.12;
  letter-spacing: -0.02em;
}

.gate-intro p {
  margin: 0;
  color: var(--sky-text-dim);
  font-size: 17px;
  line-height: 1.6;
}

/* --- the frames ---
   Three bands down the page, each the column again so its copy lines up
   with the pitch above. Inside, the name and the title on the left and the
   paragraph on the right: two columns, because a 560px paragraph under a
   title is a blog post and a page of three of them is three blog posts.
   Every other band is raised, so the eye finds the joins. On the landing
   every other band is left out instead, so the sky shows through it - see
   `.gate-frames` below, which is the only place that changes them. */
.frame {
  padding: 72px 0;
  border-top: 1px solid var(--border);
}

.frame:nth-of-type(even) {
  background: var(--bg-raised);
}

.frame-copy {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 24px;
}

.frame .frame-copy {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: 24px 64px;
  align-items: start;
}

.frame-eyebrow {
  display: inline-block;
  margin-bottom: 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: pre;
  color: var(--accent);
}

.frame-title {
  margin: 0;
  font-size: 28px;
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.frame-body {
  margin: 0;
  color: var(--text-dim);
  font-size: 17px;
  line-height: 1.65;
  /* On the title's first line, not the eyebrow's: the paragraph is what
     the title says, so it starts where the title does. */
  padding-top: 32px;
}

/* The foot: one line and the button, on the sky again so the page ends
   where it began. */
.gate-close {
  background: var(--sky);
  color: var(--sky-text);
  padding: 56px 0 64px;
}

.gate-close .frame-copy {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}

.gate-close-line {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.gate-close .primary-btn {
  padding: 10px 20px;
  font-size: 14px;
}

/* --- the landing's bands alternate: white, stars, white ---
   On How it works and the blog a band is a band: full width, one after the
   next, on the page's own white. On the landing there is a photograph
   behind everything, and a wall of white bands under the sky would cover
   it - the page would be a sky at the top and a white page under it, which
   is what it was.

   So here every other band is left out: the odd ones are white and opaque
   and cover the stars as they pass over them, and the even ones have no
   ground at all, so what is behind them is the sky. Full width, no corners
   and no gaps - the join between one band and the next is the change of
   ground, which needs no line drawn under it.

   `.gate-frames` is the landing's own wrapper (console.html) and nothing
   else has one, so scoping to it leaves the other two pages alone. Nothing
   here moves the copy: a band is the same column and the same two halves
   it is on every public page, and only its ground changes. */
.gate-frames .frame {
  background: var(--bg-raised);
  border-top: none;
}

/* The bands that are only the sky. Their copy is the sky's - the eyebrow
   included, which is the accent green everywhere else and is too dark a
   green to read on a photograph of a night. */
.gate-frames .frame:nth-of-type(even) {
  background: none;
  color: var(--sky-text);
}

.gate-frames .frame:nth-of-type(even) .frame-body {
  color: var(--sky-text-dim);
}

.gate-frames .frame:nth-of-type(even) .frame-eyebrow {
  color: var(--sky-text);
}

/* The foot is a sky band too, so the page ends on the stars the way it
   began on them - and it follows a white one, so the alternation holds all
   the way down. */
.gate-frames .gate-close {
  background: none;
}

/* --- the footer, on both halves of the app ---
   Two links, and no more: who made this, and how to say something to them.
   Both are dialogs rather than pages of their own - see console-footer.js -
   because a page of its own is a page that has to be routed, titled and kept
   from going stale, and neither of these has more to say than a box holds.

   It is drawn in the sky's palette wherever it lands, because everywhere it
   lands the sky is what is behind it: on the public pages it follows the
   closing band, which is the sky's colour, and continues it to the bottom of
   the page; in the console it is the bottom of the same photograph the rail
   stands on, so there it paints no ground of its own. */
.site-footer {
  background: var(--sky);
  color: var(--sky-text-dim);
  border-top: 1px solid var(--sky-line);
}

.site-footer-inner {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 16px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px 20px;
  flex-wrap: wrap;
}

.site-footer-mark {
  color: var(--sky-text);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.site-footer-links {
  display: flex;
  gap: 2px;
  /* The last link's own padding is the gutter, so without this the words end
     10px short of everything else down the right-hand edge. */
  margin-right: -10px;
}

.site-footer-link {
  padding: 7px 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--sky-text-dim);
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  cursor: pointer;
}

.site-footer-link:hover {
  color: var(--sky-text);
  background: var(--sky-glass);
}

.site-footer-link:focus-visible {
  outline: 2px solid var(--sky-line-strong);
  outline-offset: 1px;
}

/* On the landing the stars are behind the whole page, so the foot ends on
   them the way the closing band above it does - the alternation holds to the
   bottom. Only the ground goes: the column stays, because `.gate-frames`
   itself has no gutter and a footer that gave up its own ran its mark into
   the left edge of the screen and its links off the right. */
.gate-frames .site-footer {
  background: none;
}

/* The console's strip. It spans the rail and the page both, and the sky is
   already behind it, so it has no ground of its own and no centred column:
   its mark lines up with the rail and its links with the right-hand edge of
   the page, the way the titlebar does at the other end. */
.console-foot {
  grid-column: 1 / -1;
}

.console-foot .site-footer {
  background: none;
}

.console-foot .site-footer-inner {
  max-width: none;
  padding: 10px 24px 10px 18px;
}

/* The message. It inherits `.text-input` the way every other field on this
   page does, and changes only what makes it a box: the dialog's full width,
   and tall enough that a paragraph reads as one. */
/* The message and the address under it. A column, because the box is 560px
   at its widest and an address beside a six-line textarea would be a field
   four characters wide on a phone. */
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.contact-form .text-input { width: 100%; box-sizing: border-box; }

.contact-box {
  width: 100%;
  box-sizing: border-box;
  min-height: 120px;
  resize: vertical;
  line-height: 1.5;
  font-size: 13px;
}

/* --- who made it, in the About box ---
   The photograph is 76px because that is two of its pixels for every one of
   ours on a retina screen and no more; the card drops it rather than showing
   a broken frame if the file ever goes missing. */
.founder {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

/* The photograph and the word under it travel together, so the caption
   stays centred on the face at both widths rather than under the paragraph. */
.founder-who {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 76px;
}

.founder-photo {
  flex: 0 0 auto;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border);
}

/* Small, and quiet: it labels the face, it is not a second heading - and it
   is written the way a word is, not shouted in capitals. */
.founder-role {
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.founder-text {
  min-width: 0;
}

.founder-bio {
  margin: 0 0 10px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text);
}

.founder-link {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.founder-link:hover {
  text-decoration: underline;
}

/* The way out of a public page is a link, not a button - it goes somewhere -
   so it needs the reset `button` gets for free in styles.css. */
.console-gate a.primary-btn,
.console-gate a.ghost-btn {
  display: inline-block;
  text-decoration: none;
  line-height: 1.2;
  cursor: pointer;
}

.console-gate a.ghost-btn {
  align-self: start;
  margin-top: 20px;
}

/* --- the bar between the public pages ---
   Three addresses across the top of each of them. It sits on the sky, above
   the pitch, and is the only navigation a visitor who is not signed in has -
   the column of pages belongs to the console and is not on screen here. */
.gate-nav {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto 40px;
  padding: 0 24px;
}

.gate-nav-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 4px;
  margin-left: -12px;
}

.gate-nav-link {
  padding: 7px 12px;
  border-radius: 7px;
  color: var(--sky-text-dim);
  font-size: 14px;
  font-weight: 550;
  text-decoration: none;
}

.gate-nav-link:hover {
  color: var(--sky-text);
  background: var(--sky-glass);
}

/* Where you are, said in more than a shade: a reader who cannot tell the two
   greys apart still has the underline, and a screen reader has aria-current. */
.gate-nav-link.current {
  color: var(--sky-text);
  background: var(--sky-glass);
}

/* --- a public page that is not the landing ---
   The landing's sky carries a photograph because it is the first thing anyone
   sees. The others carry the sky's own colour: the photograph is the welcome,
   and repeating it on every page makes it wallpaper. */
.gate-sky-plain {
  background: var(--sky);
  padding: 32px 0 56px;
}

/* No form beside the title here, so the title has the width. */
.gate-top-solo {
  display: block;
}

.gate-top-solo .gate-intro {
  max-width: 760px;
}

.gate-page[hidden],
.gate-home[hidden] {
  display: none;
}

/* --- what sits beside a band's title ---
   The paragraph, and on the band about where CoderVibes runs, the places and
   the list of what "yours" is made of. One column, so they stack under the
   paragraph rather than beside it. */
.frame-side {
  display: flex;
  flex-direction: column;
}

/* The clouds, named. Not a sentence: a reader scanning for their own is
   scanning for a word, and a word in a row is found faster than a word in a
   list of commas. */
.frame-places {
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.frame-place {
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 13px;
  font-weight: 550;
}

.frame-points {
  margin: 22px 0 0;
  padding: 0 0 0 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  font-size: 15.5px;
  line-height: 1.6;
}

/* The band about where this runs, on the sky.
   It is the answer to the question that decides whether anyone can use this
   at all - whose account, whose data - so it is not one white band among six.
   It is lifted out of the numbering for the same reason: it is not a step. */
/* Two classes, not one: `.frame:nth-of-type(even)` is also two selectors
   worth of specificity, and whether this band is an even one is an accident
   of how many bands the page has. Written singly, it lost, and the band came
   out near-white text on a near-white ground - legible in the stylesheet and
   invisible on the page. */
.frame.frame-own {
  background: var(--sky);
  color: var(--sky-text);
  border-top: none;
}

.frame-own .frame-body,
.frame-own .frame-points {
  color: var(--sky-text-dim);
}

.frame-own .frame-eyebrow {
  color: var(--sky-text);
}

.frame-own .frame-place {
  border-color: var(--sky-line-strong);
  background: var(--sky-glass);
  color: var(--sky-text);
}

/* The blog before there is a blog: one band, and the way to the page that
   does have something on it. */
.frame-soon .frame-title {
  color: var(--text-dim);
}

/* --- the landing's figures ---
   One under each of the three bands (console-figures.js): the pipeline a
   session's record travels and one session going down it, a tool call held
   in front of a person, and two setups compared on the figures Performance
   draws. Each sits in the band's own column, under both halves of the copy,
   so its edges are the title's edges.

   The first and third are on white bands and the second on the sky, and the
   bands alternate by position (`.gate-frames .frame:nth-of-type(even)`), so
   the held call is written in the sky's tokens and the other two in the
   page's - a figure does not know which ground it is on, and these are the
   only three there are. If a fourth band is ever added ahead of them, the
   grounds move and so must these. */
/* Two classes, because `.frame .frame-copy` is the band's two-column grid
   and this wrapper is a `.frame-copy` too (for the column's width and
   padding): written singly, the figure landed in the grid's first column
   and drew itself in five twelfths of the width. */
.frame .frame-figure {
  display: block;
  margin-top: 40px;
}

.figure-tag {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The pipeline: four stages in a row, joined by one rule with a chevron
   before each next stage. The rule is the top border of each stage plus a
   bridge across the gap, so the line reads as one line and not four. */
.flow {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0 28px;
}

.flow-stage {
  position: relative;
  padding-top: 18px;
  border-top: 2px solid var(--accent);
}

.flow-stage:not(:last-child)::before {
  content: "";
  position: absolute;
  top: -2px;
  right: -28px;
  width: 28px;
  border-top: 2px solid var(--accent);
}

.flow-stage:not(:last-child)::after {
  content: "";
  position: absolute;
  top: -6px;
  right: -22px;
  width: 8px;
  height: 8px;
  border-top: 2px solid var(--accent);
  border-right: 2px solid var(--accent);
  transform: rotate(45deg);
}

.flow-name {
  display: block;
  font-size: 16px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.flow-who {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.flow-who li {
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: 12.5px;
  font-weight: 550;
  color: var(--text-dim);
}

.flow-what {
  margin: 12px 0 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-dim);
}

/* One session as the trail records it: a row of steps joined by a line.
   The row scrolls sideways rather than wrapping when it is wider than the
   page, because a trace is a line and a line broken in two reads as two
   sessions. Each step's left edge is its kind in colour - prompt, tool, a
   test that failed, one that passed, the pull request, production - and
   the same colours mean the same things on the Activity page. */
.journey {
  margin-top: 36px;
}

.journey-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 14px;
}

.journey-lede,
.measure-lede {
  font-size: 14px;
  color: var(--text-dim);
}

.journey-scroll {
  overflow-x: auto;
  padding-bottom: 6px;
}

.journey-strip {
  list-style: none;
  margin: 0;
  padding: 2px 0;
  display: flex;
  align-items: stretch;
  min-width: max-content;
}

.journey-step {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 12px 8px 11px;
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg-raised);
}

.journey-step + .journey-step {
  margin-left: 22px;
}

.journey-step + .journey-step::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -24px;
  width: 22px;
  border-top: 1px solid var(--border-strong);
}

.journey-tool { border-left-color: var(--border-strong); }
.journey-prompt { border-left-color: var(--accent); }
.journey-fail { border-left-color: var(--err); }
.journey-ok { border-left-color: var(--ok); }
.journey-pull { border-left-color: var(--series-1); }
.journey-prod { border-left-color: var(--best-ink); }

.journey-label {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}

.journey-note {
  font-size: 12px;
  color: var(--text-faint);
  white-space: nowrap;
}

/* A call held for a person, on the sky: the card is a pane of the sky's
   own light like the sign-in's controls, and its Allow is the orange every
   waiting card's button is - see the palette note in styles.css. The two
   answers are drawn and not wired; this is a picture of the page. */
.figure-hold {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: 24px 64px;
  align-items: start;
}

.hold-card {
  padding: 18px 20px;
  border: 1px solid var(--sky-line-strong);
  border-radius: 12px;
  background: var(--sky-glass);
  color: var(--sky-text);
}

.hold-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.hold-waiting {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--attention);
  color: var(--on-attention);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hold-since,
.hold-who {
  font-size: 13px;
  color: var(--sky-text-dim);
}

.hold-who {
  margin-bottom: 10px;
}

.hold-call {
  display: block;
  padding: 10px 12px;
  border: 1px solid var(--sky-line);
  border-radius: 8px;
  background: var(--sky);
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--sky-text);
}

.hold-why {
  margin: 12px 0 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--sky-text-dim);
}

.hold-answers {
  display: flex;
  gap: 10px;
  margin-top: 16px;
}

.hold-outcome {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--sky-line);
  font-size: 13px;
  color: var(--sky-text-dim);
}

.hold-outcome-mark {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--ok);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hold-btn {
  padding: 8px 16px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
}

.hold-allow {
  background: var(--attention);
  color: var(--on-attention);
}

.hold-deny {
  border: 1px solid var(--sky-line-strong);
  color: var(--sky-text);
}

.hold-grant-line {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--sky-text);
}

.hold-grant-keep {
  margin: 10px 0 0;
  font-size: 15px;
  line-height: 1.55;
  color: var(--sky-text-dim);
}

.hold-trail {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.hold-trail li {
  padding: 4px 10px;
  border: 1px solid var(--sky-line-strong);
  border-radius: 999px;
  background: var(--sky-glass);
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--sky-text);
}

/* Two setups compared: a legend, then a row per figure with a thin bar for
   each setup, its value beside it in text rather than on the colour. The
   hues are the chart palette's first two, in the order the legend gives
   them, so a setup keeps its colour from one row to the next; the better
   value is the bolder number, so the direction of good is said without a
   third colour. */
.measure-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}

.measure-legend {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  font-size: 13.5px;
  font-weight: 550;
}

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

.measure-swatch {
  width: 12px;
  height: 12px;
  border-radius: 3px;
}

.measure-series-1 .measure-swatch,
.measure-series-1 .measure-fill { background: var(--series-1); }
.measure-series-2 .measure-swatch,
.measure-series-2 .measure-fill { background: var(--series-2); }

.measure-rows {
  margin-top: 18px;
}

.measure-row {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 5fr);
  gap: 8px 24px;
  align-items: center;
  padding: 14px 0;
  border-top: 1px solid var(--border);
}

.measure-label {
  font-size: 14.5px;
  font-weight: 550;
}

.measure-bars {
  display: grid;
  gap: 6px;
}

.measure-bar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 64px;
  align-items: center;
  gap: 10px;
}

.measure-fill {
  display: block;
  width: var(--w);
  height: 10px;
  border-radius: 0 4px 4px 0;
}

.measure-value {
  font-size: 13.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

.measure-best .measure-value {
  font-weight: 650;
  color: var(--text);
}

.measure-note {
  margin: 22px 0 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-faint);
}

/* --- How it works' figures ---
   One under each of the six bands (console-figures.js, `howFigureFor`).
   Access and Measure draw the landing's held call and comparison; the four
   here are the page's own. Every band on this page is white except Your
   environment, which is the sky (`.frame-own`), so the boundary figure is
   written in the sky's tokens and the rest in the page's. */

/* Connect: the one line, and what comes in through it. */
.wire-line {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--bg-inset);
  font-family: var(--font-mono);
  font-size: 15px;
  overflow-x: auto;
}

.wire-prompt {
  color: var(--text-faint);
}

.wire-cmd {
  white-space: nowrap;
}

.wire-rows {
  margin-top: 20px;
  display: grid;
  gap: 12px;
}

.wire-row {
  display: grid;
  grid-template-columns: 140px minmax(0, 1fr);
  gap: 8px 16px;
  align-items: baseline;
}

.wire-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.wire-chips {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.wire-chips li {
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 13px;
  font-weight: 550;
}

/* Your environment: what is inside the reader's account, and what is not.
   Three columns - the machines, the box, the vendors - with the box drawn
   as a dashed edge, since a boundary is a line and not a fill. On the sky,
   so the whole of it is the sky's tokens. */
.figure-bound {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 6fr) minmax(0, 3fr);
  gap: 24px;
  align-items: center;
  color: var(--sky-text);
}

.bound-side-name {
  display: block;
  font-size: 15px;
  font-weight: 650;
}

.bound-side-note {
  margin: 6px 0 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--sky-text-dim);
}

.bound-box {
  position: relative;
  padding: 26px 18px 18px;
  border: 1px dashed var(--sky-line-strong);
  border-radius: 14px;
}

.bound-box-name {
  position: absolute;
  top: -9px;
  left: 16px;
  padding: 0 8px;
  background: var(--sky);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sky-text);
}

.bound-nodes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.bound-node {
  padding: 12px 14px;
  border: 1px solid var(--sky-line-strong);
  border-radius: 10px;
  background: var(--sky-glass);
}

.bound-node-name {
  display: block;
  font-size: 14px;
  font-weight: 650;
}

.bound-node-note {
  display: block;
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--sky-text-dim);
}

/* Observe: a session live, then a pipeline run under it. One column at
   every width: side by side, each pane had half the column and the strip's
   last step - the one still running, which is the point - was off the edge
   at 1280px. The run is capped so four steps do not stretch to a third of
   the page each. */
.figure-watch {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 28px;
  align-items: start;
}

.dag,
.dag-reason {
  max-width: 760px;
}

.watch-title {
  margin-bottom: 12px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.watch-note,
.dag-reason,
.seek-note {
  margin: 14px 0 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-faint);
}

/* A step still going: the accent, and the only thing on the page that
   pulses - an agent is running, which is the one case a pulse is honest. */
.journey-live {
  border-left-color: var(--accent);
}

.journey-live .journey-note {
  color: var(--accent);
}

/* The run: four steps in a row, each named, with its state and how long it
   took under the name. The state is a word as well as a colour. */
.dag {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}

.dag-step {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-top-width: 3px;
  border-radius: 8px;
  background: var(--bg-raised);
}

.dag-step + .dag-step::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -11px;
  width: 10px;
  border-top: 1px solid var(--border-strong);
}

.dag-label {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 600;
}

.dag-state {
  font-size: 12px;
  font-weight: 600;
  text-transform: capitalize;
}

.dag-took {
  font-size: 12px;
  color: var(--text-faint);
}

.dag-passed { border-top-color: var(--ok); }
.dag-passed .dag-state { color: var(--ok-text); }
.dag-failed { border-top-color: var(--err); }
.dag-failed .dag-state { color: var(--err-text); }
.dag-blocked { border-top-color: var(--bar-rest); }
.dag-blocked .dag-state { color: var(--text-faint); }

.dag-reason {
  padding: 8px 12px;
  border-radius: 6px;
  background: var(--err-bg);
  color: var(--err-text);
  font-family: var(--font-mono);
  font-size: 12.5px;
}

/* Search: one question in a box, and its three kinds of answer under it. */
.seek-box {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--bg-inset);
  font-size: 16px;
}

.seek-mark {
  color: var(--text-faint);
  font-size: 18px;
}

.seek-hits {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.seek-hit {
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-raised);
}

.seek-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
}

.seek-kind {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.seek-title {
  font-size: 15px;
  font-weight: 650;
}

.seek-tool .seek-title,
.seek-trail .seek-title {
  font-family: var(--font-mono);
  font-size: 14px;
}

.seek-quote {
  margin: 6px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-dim);
}

.seek-meta {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  font-size: 12.5px;
  color: var(--text-faint);
}

/* The held call on a white band (Access, on How it works): the same figure
   as the landing's, restated in the page's tokens - the card a bordered
   pane on white, the call a raised code block. */
.figure-on-page .hold-card {
  border-color: var(--border-strong);
  background: var(--bg-raised);
  color: var(--text);
}

.figure-on-page .hold-since,
.figure-on-page .hold-who,
.figure-on-page .hold-why,
.figure-on-page .hold-outcome,
.figure-on-page .hold-grant-keep {
  color: var(--text-dim);
}

.figure-on-page .hold-call {
  border-color: var(--border);
  background: var(--bg-inset);
  color: var(--text);
}

.figure-on-page .hold-deny {
  border-color: var(--border-strong);
  color: var(--text);
}

.figure-on-page .hold-outcome {
  border-top-color: var(--border);
}

.figure-on-page .hold-grant-line {
  color: var(--text);
}

.figure-on-page .hold-trail li {
  border-color: var(--border-strong);
  background: var(--bg-inset);
  color: var(--text);
}

/* The figures move, once each, when they come into view (`seen`, set by
   console-figures.js): the stages light up in order with the record running
   the rule between them, the session's steps arrive one after the next, the
   held call is allowed and the merge lands, the bars grow to their values.
   The motion says the order things happen in, which is the point of every
   one of these figures; nothing loops, because a thing that keeps moving
   keeps taking the eye back.

   All of it is inside the no-preference gate, so a reader who asked for no
   motion never has a hidden state at all - not even the one that waits for
   `seen`. The gate also covers the pulse, which is the one thing here that
   would be pure decoration standing still, so it is not drawn there. */
.flow-pulse {
  display: none;
}

@media (prefers-reduced-motion: no-preference) {
  .figure-flow .flow-stage,
  .figure-flow .journey-step,
  .figure-hold .hold-card,
  .figure-hold .hold-grant,
  .figure-hold .hold-trail li,
  .figure-hold .hold-outcome {
    opacity: 0;
  }

  .figure-flow.seen .flow-stage {
    animation: figure-in 520ms ease-out both;
    animation-delay: calc(var(--i) * 420ms);
  }

  .figure-flow.seen .flow-pulse {
    display: block;
    position: absolute;
    top: -6px;
    left: 0;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 4px var(--accent-dim);
    animation: flow-run 1700ms cubic-bezier(0.4, 0, 0.6, 1) both;
  }

  .figure-flow.seen .journey-step {
    animation: figure-in 380ms ease-out both;
    animation-delay: calc(1700ms + var(--i) * 170ms);
  }

  .figure-hold.seen .hold-card {
    animation: figure-in 520ms ease-out both;
  }

  .figure-hold.seen .hold-grant {
    animation: figure-in 520ms ease-out 300ms both;
  }

  /* The answer: Allow presses at a second and a half, then the outcome
     line and the trail's "used" and "expired" arrive in the order they
     happened. */
  .figure-hold.seen .hold-allow {
    animation: hold-press 700ms ease-in-out 1500ms both;
  }

  .figure-hold.seen .hold-outcome {
    animation: figure-in 520ms ease-out 2300ms both;
  }

  .figure-hold.seen .hold-trail li {
    animation: figure-in 380ms ease-out both;
    animation-delay: calc(600ms + var(--i) * 900ms);
  }

  /* `--i` is the row's (console-figures.js `stagger`), inherited by its
     two bars, so a row's pair grows together and the next row after it. */
  .figure-measure .measure-fill {
    width: 0;
    transition: width 900ms cubic-bezier(0.2, 0.7, 0.2, 1);
    transition-delay: calc(var(--i) * 140ms);
  }

  .figure-measure.seen .measure-fill {
    width: var(--w);
  }

  /* How it works. Connect: the line is typed, then the names arrive in
     order. Your environment: the box draws its edge, then the nodes fill
     it. Observe: the session's steps arrive and the last one keeps going;
     the run's steps go from queued to their state one after the next, and
     the reason comes last. Search: the question is typed, then the answers
     arrive. */
  .figure-wire .wire-cmd,
  .figure-seek .seek-query {
    display: inline-block;
    max-width: 0;
    overflow: hidden;
    white-space: nowrap;
    vertical-align: bottom;
  }

  .figure-wire.seen .wire-cmd {
    animation: figure-type 1400ms steps(52) both;
  }

  .figure-seek.seen .seek-query {
    animation: figure-type 1100ms steps(32) both;
  }

  .figure-wire .wire-chips li,
  .figure-bound .bound-node,
  .figure-bound .bound-side,
  .figure-watch .journey-step,
  .figure-watch .dag-reason,
  .figure-seek .seek-hit {
    opacity: 0;
  }

  .figure-wire.seen .wire-chips li {
    animation: figure-in 320ms ease-out both;
    animation-delay: calc(1300ms + var(--i) * 110ms);
  }

  .figure-bound .bound-box {
    border-color: transparent;
    transition: border-color 700ms ease-out;
  }

  .figure-bound.seen .bound-box {
    border-color: var(--sky-line-strong);
  }

  .figure-bound.seen .bound-node {
    animation: figure-in 420ms ease-out both;
    animation-delay: calc(500ms + var(--i) * 260ms);
  }

  .figure-bound.seen .bound-side {
    animation: figure-in 520ms ease-out 1500ms both;
  }

  .figure-watch.seen .journey-step {
    animation: figure-in 380ms ease-out both;
    animation-delay: calc(var(--i) * 420ms);
  }

  .figure-watch.seen .journey-live .journey-note {
    animation: journey-going 1600ms ease-in-out 1800ms infinite;
  }

  .figure-watch .dag-step {
    border-top-color: var(--bar-rest);
  }

  .figure-watch .dag-step .dag-state,
  .figure-watch .dag-step .dag-took {
    opacity: 0;
  }

  .figure-watch.seen .dag-passed {
    animation: dag-pass 500ms ease-out both;
    animation-delay: calc(400ms + var(--i) * 650ms);
  }

  .figure-watch.seen .dag-failed {
    animation: dag-fail 500ms ease-out both;
    animation-delay: calc(400ms + var(--i) * 650ms);
  }

  .figure-watch.seen .dag-blocked {
    animation: dag-block 500ms ease-out both;
    animation-delay: calc(400ms + var(--i) * 650ms);
  }

  .figure-watch.seen .dag-step .dag-state,
  .figure-watch.seen .dag-step .dag-took {
    animation: figure-in 300ms ease-out both;
    animation-delay: calc(500ms + var(--i) * 650ms);
  }

  .figure-watch.seen .dag-reason {
    animation: figure-in 420ms ease-out 2600ms both;
  }

  .figure-seek.seen .seek-hit {
    animation: figure-in 420ms ease-out both;
    animation-delay: calc(1200ms + var(--i) * 300ms);
  }
}

@keyframes figure-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

@keyframes flow-run {
  from { left: 0; }
  to { left: calc(100% - 10px); }
}

@keyframes figure-type {
  from { max-width: 0; }
  to { max-width: 100%; }
}

/* The step still running breathes - the one pulse on the page, and it is
   an agent at work. Gated with the rest, so a reader who asked for no
   motion reads "running" as a word. */
@keyframes journey-going {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@keyframes dag-pass {
  from { border-top-color: var(--bar-rest); }
  to { border-top-color: var(--ok); }
}

@keyframes dag-fail {
  from { border-top-color: var(--bar-rest); }
  to { border-top-color: var(--err); }
}

@keyframes dag-block {
  from { border-top-color: var(--bar-rest); }
  to { border-top-color: var(--bar-rest); }
}

@keyframes hold-press {
  0%, 100% { transform: none; filter: none; }
  40% { transform: scale(0.94); filter: brightness(1.2); }
  60% { transform: scale(0.94); filter: brightness(1.2); }
}

/* The sign-in itself, since this is the only page there is. Left-aligned
   inside a centred card: a form whose every field is centred is a form nobody
   can scan down. */
.signin-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  text-align: left;
  padding: 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-raised);
}

.signin-providers {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.provider-btn {
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.provider-btn:hover { background: var(--bg-hover); }

.signin-divider {
  color: var(--text-faintest);
  font-size: 11px;
  text-align: center;
}

.signin-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.signin-input {
  padding: 9px 11px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}

.signin-input:focus {
  outline: none;
  border-color: var(--accent-dim);
}

.signin-links {
  display: flex;
  justify-content: space-between;
  gap: 10px;
}

.link-btn {
  border: none;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
  padding: 0;
}

.link-btn:hover { color: var(--accent); text-decoration: underline; }

.signin-said {
  margin: 0;
  color: var(--ok-text);
  font-size: 12px;
}

/* Said plainly, because a passwordless server on an open network is not a
   detail somebody should have to infer from the absence of a password box.
   `.console-warning` is the same block wherever else an installation has to
   admit something about itself - a GitHub App that was never registered, say,
   which is likewise only visible as an absence. */
.signin-warning,
.console-warning {
  margin: 0;
  padding: 8px 10px;
  border: 1px solid var(--warn-border);
  border-radius: 6px;
  background: var(--warn-bg);
  color: var(--warn-text);
  font-size: 11.5px;
  line-height: 1.5;
}

/* --- the form on the sky ---
   The same form, on the photograph: no card. A white card on a night sky
   is a lit window, and the eye goes to the window and not to what it says.
   So the controls sit on the sky as it is, each a faint pane of the sky's
   own light - a hairline and a hint of white - and the one solid thing is
   the button that signs you in. Scoped to the sky, so the panel stays a
   card wherever else it is drawn. */
.gate-sky .signin-panel {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  gap: 12px;
}

.gate-sky .provider-btn,
.gate-sky .signin-input {
  padding: 11px 14px;
  border: 1px solid var(--sky-line);
  border-radius: 9px;
  background: var(--sky-glass);
  color: var(--sky-text);
  font-size: 13.5px;
  backdrop-filter: blur(8px);
  transition: background 0.15s, border-color 0.15s;
}

.gate-sky .provider-btn:hover {
  background: var(--sky-glass-hover);
  border-color: var(--sky-line-strong);
}

.gate-sky .signin-input::placeholder { color: var(--sky-text-dim); }

.gate-sky .signin-input:focus {
  border-color: var(--sky-line-strong);
  background: var(--sky-glass-hover);
}

.gate-sky .signin-form .primary-btn {
  padding: 11px 14px;
  border-radius: 9px;
  font-size: 13.5px;
}

.gate-sky .signin-divider,
.gate-sky .link-btn {
  color: var(--sky-text-dim);
}

.gate-sky .link-btn:hover { color: var(--sky-text); }

.gate-sky .signin-said { color: var(--sky-text); }

.gate-sky .signin-warning {
  border-color: var(--sky-line);
  background: var(--sky-glass);
  color: var(--sky-text-dim);
}

/* --------------------------------------------------- the column and the page */

/* The signed-in console: the rail, the page, and the footer under both.
   The photograph the signed-out page opens on is behind the grid, and the
   footer strip is the only part of the grid that lets it through - the rail
   and the page are both white over it. It stays on the grid rather than
   moving onto the strip now that the strip is all that shows it, because
   `cover` on a 40px box is that photograph blown up until a handful of
   stars fill it; on the grid the strip is the bottom of the same picture at
   the same size the landing shows it. */
.console {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: minmax(0, 1fr) auto;
  background: var(--sky) url("sky.jpg") center / cover no-repeat;
}

/* `display: grid` above beats the `hidden` attribute's own display: none, so
   the signed-out page had an empty column sitting under the sign-in form. */
.console[hidden] {
  display: none;
}

/* The pages. It was a panel that slid over the page from behind the mark;
   it is a column now, and the column is the whole of the navigation - each
   page's own list of things is in the page, where there is room for it. */
.app-sidebar {
  padding: 10px 8px;
  background: var(--bg-raised);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

/* Which workspace, above the pages, because it decides what they hold.
   A native select: the list is short, it is changed rarely, and a control
   the platform draws is one the platform makes reachable. */
.sidebar-workspace {
  margin-bottom: 10px;
}

.sidebar-workspace-pick {
  width: 100%;
  min-height: 36px;
  padding: 6px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  box-sizing: border-box;
  cursor: pointer;
}

.sidebar-workspace-pick:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sidebar-link {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border-radius: 7px;
  color: var(--text-dim);
  font-size: 13px;
  text-align: left;
  text-decoration: none;
  box-sizing: border-box;
  cursor: pointer;
}

.sidebar-link:hover {
  background: var(--bg-hover);
  color: var(--text);
}

/* Where you are: an outlined chip, and the name in it black and heavy. On a
   photograph the outline was a patch of paint over the stars and the weight
   was left to carry it alone; on a white rail weight alone is a difference
   you have to go looking for down eight names, and the chip is what you find
   your place by without reading any of them. */
.sidebar-link.active {
  background: var(--bg-inset);
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--border-strong);
  font-weight: 650;
}

.sidebar-icon {
  color: var(--accent);
  font-size: 12px;
  line-height: 1;
}

/*
 * The dot beside a name is the whole status indicator, and idle is the
 * absence of colour - the same rule the assistant pane's activity strip
 * follows. A page where nothing is happening should not be lit up.
 */
.rail-dot {
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--text-faintest);
}

/*
 * Two kinds of lit. An agent that is working pulses, because it is a thing in
 * motion and the question is "is it still going". A machine that is up, or
 * a repo that is live, is simply on - a pulsing dot next to an app
 * would read as activity that is not happening.
 */
.list-row.live .rail-dot { background: var(--ok); }

.list-row.busy .rail-dot {
  background: var(--accent);
  animation: rail-pulse 1.6s ease-in-out infinite;
}

@keyframes rail-pulse {
  50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .list-row.busy .rail-dot { animation: none; }
}

/* ------------------------------------------ finding and picking machines */

/* The inventory's toolbar: a search box, a row of filters, and a selection
   bar, above the list. In the main pane, where a list with five columns has
   room; the rail on Machines holds the two views, not the machines. */
.list-search {
  width: 100%;
  margin-bottom: 8px;
  padding: 7px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}

.list-search:focus {
  outline: none;
  border-color: var(--accent-dim);
}

.list-tools {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}

.list-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

/* A filter is a chip that can be pressed. Pressed reads as the row's own
   selected state, so the two vocabularies - which rows, which filters - are
   one colour. */
.filter-chip {
  padding: 2px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 10.5px;
  cursor: pointer;
  white-space: nowrap;
}

.filter-chip:hover { background: var(--bg-hover); color: var(--text); }

.filter-chip[aria-pressed="true"] {
  background: var(--bg-active);
  border-color: var(--accent-dim);
  color: var(--text);
}

/* ------------------------------------------------------ the filter dropdown

   A filter is a labelled button that says what is picked, with the list
   under it (console-filters.js). It replaced a row of chips per filter:
   the chips were fine at four options and unreadable at forty, and forty
   is what a real installation's repo and executor lists are.

   The wrapper is the positioning context, so the list hangs off the
   button and not off the page. `.console-main` scrolls, so a list that
   runs past the fold lengthens the scroll rather than being cut off. */
.filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px 14px;
  margin-bottom: 14px;
}

.filter-picker {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.filter-picker-label {
  flex: none;
  color: var(--text-dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.filter-picker-shell {
  position: relative;
  min-width: 0;
}

/* The closed control. It carries the whole of what the filter is doing,
   since the options it is hiding are no longer on the page. */
.filter-picker-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 240px;
  padding: 3px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
}

.filter-picker-btn:hover { background: var(--bg-hover); }

.filter-picker-btn:focus-visible {
  outline: none;
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 2px var(--accent-dim);
}

/* Set reads as a pressed chip did - the same colour the row vocabulary
   uses for selected, so "this page is narrowed" is one colour everywhere. */
.filter-picker.is-set .filter-picker-btn {
  background: var(--bg-active);
  border-color: var(--accent-dim);
}

.filter-picker-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.filter-picker-note {
  flex: none;
  color: var(--text-dim);
  font-size: 10.5px;
}

/* The caret, drawn rather than typed: a glyph in the button's text would
   be read out as a word by a screen reader. */
.filter-picker-caret {
  flex: none;
  margin-left: auto;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid var(--text-dim);
}

.filter-picker-pop {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  min-width: 100%;
  max-width: min(320px, calc(100vw - 32px));
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg-raised);
  box-shadow: 0 12px 32px var(--shadow-soft);
}

.filter-picker-find {
  width: 100%;
  margin-bottom: 6px;
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.filter-picker-find:focus {
  outline: none;
  border-color: var(--accent-dim);
}

/* Nine rows and then it scrolls: enough that most lists are whole, few
   enough that a long one does not cover the page it is filtering. */
.filter-picker-list {
  display: flex;
  flex-direction: column;
  max-height: 252px;
  overflow-y: auto;
}

/* The list takes focus so the arrows work when there is no box above it;
   the ring the browser draws for that says nothing the active row does not,
   and it draws around the whole list rather than around a choice. */
.filter-picker-list:focus { outline: none; }

.filter-picker-option {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 8px;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

/* A set's rows carry a box, because "on" as a background alone reads as a
   hover on whichever row the cursor is over. The tick is drawn - two
   borders rotated - so there is no glyph in the row for a screen reader
   to read out in the middle of the option's name; `aria-selected` says
   it there. */
.filter-picker-tick {
  position: relative;
  flex: none;
  width: 13px;
  height: 13px;
  border: 1px solid var(--border-strong);
  border-radius: 3px;
  background: var(--bg-inset);
}

.filter-picker-tick.is-on {
  border-color: var(--ok);
  background: var(--ok);
}

.filter-picker-tick.is-on::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 3px;
  height: 6px;
  border: solid var(--on-accent);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}

/* The name takes the room and the note gives it up. Both were free to
   shrink, and with `margin-left: auto` on the note the name was the one
   that lost: a list whose notes are long - the one filter over every facet
   has "Place to run · 147 sessions" on every row - drew a column of notes
   with nothing beside them. The name is the answer to "which one is this";
   the note is what kind of thing it is, and an ellipsis on that costs
   nothing. */
.filter-picker-option-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.filter-picker-option .filter-picker-note {
  flex: 0 1 auto;
  margin-left: auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.filter-picker-option:hover,
.filter-picker-option.is-active { background: var(--bg-hover); }

.filter-picker-option.is-picked {
  background: var(--bg-active);
  color: var(--text);
}

.filter-picker-none {
  margin: 0;
  padding: 6px 8px;
  color: var(--text-dim);
  font-size: 12px;
}

.list-select {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.list-select-all {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-faint);
  font-size: 11.5px;
  cursor: pointer;
}

/* The actions menu. A native <details>, so it opens, closes and dismisses on
   Escape without a line of script; the panel is positioned under the button
   rather than pushing the list down. */
.list-actions {
  position: relative;
}

.list-actions summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}

.list-actions summary::-webkit-details-marker { display: none; }

.list-actions.disabled summary {
  opacity: 0.5;
  cursor: default;
}

.list-actions .menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 5;
  min-width: 190px;
  padding: 4px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--bg-raised);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  display: flex;
  flex-direction: column;
}

.menu-item {
  padding: 7px 10px;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}

.menu-item:hover { background: var(--bg-hover); }
.menu-item.danger { color: var(--err); }
.menu-item:disabled { color: var(--text-faintest); cursor: default; }

/* --------------------------------------------------------------- search */

/* One box and the rows under it. The box is a form so Enter searches; the
   field takes the width and the two buttons sit beside it - Search, and AI
   assist for the same question read by the model - and on a phone they
   wrap under the field rather than squeezing it (see the phone block at
   the end). */
.search-form {
  display: flex;
  gap: 8px;
  margin: 0 0 12px;
}
.search-form .search-input {
  flex: 1 1 240px;
  min-width: 0;
}
.search-form .search-submit,
.search-form .search-assist {
  flex: 0 0 auto;
}

/* A hit is a stacked row: the name (a button to its page) and its chips on
   the head, the quote and what it reached for in the body. */
.search-name {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  text-align: left;
}
.search-name:hover {
  text-decoration: underline;
}
.search-hit .row-head {
  flex-wrap: wrap;
}

/* The quote: a person's or a model's words, set as text, kept to a few
   lines and broken anywhere so a long token cannot widen the page. */
.search-snippet {
  margin: 4px 0 6px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.search-why {
  margin: 4px 0 0;
  color: var(--text-dim);
  font-size: 12px;
}
.search-foot {
  margin-top: 16px;
}

/* The model's half of the page, drawn only once it has answered: the turns
   it has answered and the box that carries the chat on. Raised, so it
   reads as one thing under the box and the rows below it as what was
   found. */
.search-answer {
  margin: 0 0 16px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}
.search-answer-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  align-items: center;
  justify-content: space-between;
}
.search-answer-head .panel-heading { margin: 0; }

/* The buttons on this page that spend anything - AI assist in the search
   box, Ask under the answer: a yellow tint, the colour this console uses
   for "this is not free, and it will take a moment" - the same amber as a
   card waiting on somebody. */
.explain-btn {
  padding: 7px 13px;
  border-radius: 7px;
  border: 1px solid var(--warn-border);
  background: var(--warn-bg);
  color: var(--warn-text);
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
}
.explain-btn:hover { border-color: var(--warn-text); }
.explain-btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* One exchange: what was asked, then what came back. The question is set
   apart by weight rather than by a bubble - this is a console, not a
   messenger - and the turns stack oldest first. */
.search-turn {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.search-turn-question {
  margin: 0 0 6px;
  font-weight: 500;
  overflow-wrap: anywhere;
}

/* The box that carries the chat on, under the last answer. */
.search-chat-form {
  display: flex;
  gap: 8px;
  margin: 12px 0 0;
}
.search-chat-form .search-chat-input {
  flex: 1 1 240px;
  min-width: 0;
}
.search-chat-form .search-chat-send {
  flex: 0 0 auto;
}
.search-answer-text {
  margin: 0;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.search-cite {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
}
.search-answer-note {
  margin: 8px 0 0;
  color: var(--text-dim);
  font-size: 12px;
}

/* Which thing is searched and how far back: two rows of chips under the
   box, side by side while there is room. */
.search-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  align-items: flex-start;
  margin: 0 0 12px;
}
.search-controls .list-filters,
.search-controls .filter-row {
  margin: 0;
}

/* The histogram: one bar per slice of the range, each a button. The bars
   are thin and rounded at the data end, sit on a baseline, and stack the
   allowed share under the stopped one; the stopped share is hatched as
   well as coloured, so the two parts are told apart without the colour. */
.search-histogram {
  margin: 0 0 16px;
  padding: 10px 12px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}
.search-histogram-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin-bottom: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.search-histogram-title {
  font-weight: 600;
  color: var(--text);
}
.search-histogram-legend {
  display: flex;
  gap: 12px;
}
.search-histogram-key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.search-histogram-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--accent);
}
.search-bucket-chip {
  cursor: pointer;
  font: inherit;
  font-size: 12px;
}
.search-histogram-bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 96px;
  border-bottom: 1px solid var(--border);
}
.search-histogram-bar {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: 4px 4px 0 0;
}
.search-histogram-bar:hover,
.search-histogram-bar:focus-visible {
  background: var(--bg-hover);
  outline: none;
}
.search-histogram-bar.is-open {
  background: var(--bg-active);
}
.search-histogram-bar.is-empty {
  cursor: default;
}
.search-histogram-stack {
  display: flex;
  flex-direction: column-reverse;
  width: 100%;
  min-height: 0;
  gap: 2px;
  border-radius: 4px 4px 0 0;
  overflow: hidden;
}
.search-histogram-piece {
  display: block;
  flex: 0 0 auto;
  min-height: 2px;
  background: var(--accent);
}
.search-histogram-piece.is-allowed,
.search-histogram-swatch.is-allowed {
  background: var(--accent);
}
.search-histogram-piece.is-stopped,
.search-histogram-swatch.is-stopped {
  background: var(--attention);
  background-image: repeating-linear-gradient(135deg, transparent 0 3px, rgba(255, 255, 255, 0.45) 3px 5px);
}
.search-histogram-piece.is-open-ask,
.search-histogram-swatch.is-open-ask {
  background: var(--text-faintest);
}
.search-histogram-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-faint);
}
.search-histogram-readout {
  min-height: 1.4em;
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--text-dim);
}

/* The trail's rows: the state chip first, the agent's words under, the
   links to the session, the agent and the tool as chips. */
.search-entry .row-head {
  flex-wrap: wrap;
}
.search-entry-decision {
  margin: 2px 0 6px;
  color: var(--text-dim);
  font-size: 12px;
}
.search-entry-links {
  margin-top: 4px;
}
.search-entry-links .chip-link {
  cursor: pointer;
  font: inherit;
  font-size: 12px;
}

/* The sums: the tiles, then the top lists in a grid that wraps. A top list
   is a name, a bar for the count against the largest, and the count. */
.search-top-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 8px 20px;
  margin-bottom: 16px;
}
.search-top .panel-heading {
  margin-bottom: 6px;
}
.search-top-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.search-top-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(40px, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
  font-size: 13px;
}
.search-top-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  color: var(--text);
  font: inherit;
  font-size: 13px;
}
button.search-top-name {
  cursor: pointer;
}
button.search-top-name:hover {
  text-decoration: underline;
}
.search-top-bar {
  display: block;
  height: 6px;
  border-radius: 3px;
  background: var(--bar-rest);
  overflow: hidden;
}
.search-top-fill {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: var(--accent);
}
.search-top-count {
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}
.search-top-note {
  grid-column: 1 / -1;
  margin-top: -2px;
  font-size: 11px;
  color: var(--text-faint);
}

/* ------------------------------------------------------- a list of things */

/* One row per thing, in the main pane: a name with a line under it, then a
   cell per fact. `--cells` is how many, set by the table so the grid needs
   no rule per page. The dot beside the name is "alive" - see .rail-dot. */
.list-table {
  --cells: 3;
  display: grid;
  grid-template-columns: minmax(0, 2.4fr) repeat(var(--cells), minmax(0, 1fr));
  margin-bottom: 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.list-table.pickable {
  grid-template-columns: 30px minmax(0, 2.4fr) repeat(var(--cells), minmax(0, 1fr));
}

.list-head,
.list-row {
  display: contents;
}

.list-head > * {
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-inset);
  color: var(--text-faint);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

.list-row > * {
  display: flex;
  align-items: center;
  min-width: 0;
  padding: 8px 10px;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
}

.list-row:first-of-type > * { border-top: none; }

.list-row .list-pick,
.list-head .list-pick-gap {
  padding: 8px 0 8px 10px;
  margin: 0;
}

.list-open {
  gap: 9px;
  border: none;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.list-open:hover .list-name { color: var(--accent); }

/* A row that opens nothing - a skill, which has no page of its own. It
   reads as a row, and not as a thing to press. */
.list-still { cursor: default; }
.list-still:hover .list-name { color: var(--text); }

.list-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.list-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.list-note {
  color: var(--text-faint);
  font-size: 11.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.list-cell {
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A number shaded by where it stands in its own column (`heatRanks` in
   console-list.js). The wash is on the number and not on the cell, which
   fills its column and would band the table the width of a column; a pill
   the width of the number leaves it a table to read and still a column of
   colour to scan. No border and no weight - the wash is the whole of it -
   and the number goes to full text, because a dim number on a wash is two
   things asking for the same contrast. */
.heat {
  display: inline-block;
  min-width: 34px;
  padding: 1px 7px;
  border-radius: 4px;
  color: var(--text);
  text-align: inherit;
}

.heat-1 { background: var(--heat-1); }
.heat-2 { background: var(--heat-2); }
.heat-3 { background: var(--heat-3); }
.heat-4 { background: var(--heat-4); }
.heat-5 { background: var(--heat-5); }

/* The Executors list's Sessions cell: the count, and under it how many are
   running right now. The Pull requests and Cost cells beside it are plain
   text - three stacked pairs put a paragraph back in the table that giving
   each figure a column was meant to take out of it. A phone drops all three
   (.list-aside says them again under the name). */
.list-standing {
  display: block;
  white-space: normal;
  line-height: 1.35;
}
.list-standing-note {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
}

/* The standing said again under the name, for the phone only: on the
   desktop the cell says it. */
.list-aside { display: none; }

.list-empty {
  margin: 0 0 16px;
  color: var(--text-faint);
  font-size: 12.5px;
}

/* Under the title: the counts, or what the page is for, in one line. */
.detail-summary {
  margin: -4px 0 12px;
  color: var(--text-dim);
  font-size: 12.5px;
}

/* ------------------------------------------------- the numbers and the bars */

/* Metric tiles - the Machines page, Account, Performance. Six that wrap. */
.metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
  margin-bottom: 16px;
}

.metric {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}

.metric-value {
  font-size: 22px;
  font-weight: 600;
  line-height: 1.1;
}

.metric-label {
  margin-top: 4px;
  color: var(--text-dim);
  font-size: 12px;
}

.metric-note {
  margin-top: 2px;
  color: var(--text-faint);
  font-size: 11px;
}

/* ------------------------------------------------------------------ help
 *
 * The "?" beside a word the page cannot define in the space it has - see
 * console-dom.js `helpMark`. The bubble is positioned against the mark and
 * never inside a cell's overflow, because a table cell clips and the answer
 * is wider than the column it explains.
 */
.help {
  position: relative;
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  /* The word is what the reader is scanning; the mark is beside it. */
  vertical-align: middle;
}

.help-mark {
  width: 14px;
  height: 14px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: var(--bg);
  color: var(--text-faint);
  font-size: 10px;
  line-height: 1;
  cursor: pointer;
}

.help-mark:hover,
.help-mark[aria-expanded="true"] {
  border-color: var(--accent);
  color: var(--accent);
}

.help-mark:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.help-bubble {
  position: absolute;
  z-index: 30;
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 280px;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-raised);
  box-shadow: 0 4px 14px rgb(0 0 0 / 12%);
  color: var(--text);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.45;
  /* Normal text in a place that may be a table head or a metric label,
     both of which are uppercase, tracked out, or clipped to one line. */
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  white-space: normal;
}

/* A column head that carries one: the "?" must not push the head onto a
   second line, which would change the height of every row's header. */
.help-head {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}

/* A head cell clips, so that a long column name is cut rather than wrapped
   - and it clipped the bubble too, which no z-index can get out of: a
   clipping ancestor is not something a stacking order escapes. The head is
   the one row where a cell has something to show outside itself, and its
   labels are short enough not to need the cut. The body rows keep theirs. */
.list-head > * {
  overflow: visible;
}

/* The one number that asks for something to be done. */
.metric-warn {
  border-color: var(--warn-border);
  background: var(--warn-bg);
}

.metric-warn .metric-value { color: var(--warn-text); }

/* The bars. The SVG scales to the panel; the labels are in the SVG's own
   units so they scale with it, which at phone width is small but legible. */
.chart {
  display: block;
  width: 100%;
  height: auto;
}

.chart-bar { fill: var(--accent); }
.chart-bar.empty { fill: var(--border); }
.chart-grid { stroke: var(--border); stroke-dasharray: 2 3; }
.chart-axis { stroke: var(--border-strong); }
.chart-label { fill: var(--text-faint); font-size: 11px; }

/* ------------------------------------------------------------- platforms */

/* Each vendor is one colour, and the same colour on the card, in the chart
   and in the legend - the class carries it, so a fourth vendor is one line
   here. The tints are the shared palette's; Fly keeps the accent because it
   was the only bar before. */
.platform-fly { --platform: var(--accent); }
.platform-e2b { --platform: var(--tint-0); }
.platform-daytona { --platform: var(--tint-3); }
.platform-local { --platform: var(--tint-2); }
.platform-other { --platform: var(--text-faint); }

.chart-bar.platform-fly,
.chart-bar.platform-e2b,
.chart-bar.platform-daytona,
.chart-bar.platform-local,
.chart-bar.platform-other { fill: var(--platform); }

/* Working time is the narrow bar inside the stack: the same on every
   platform, since the tool ledger does not say where a call ran. */
.chart-bar.working { fill: var(--text); opacity: 0.55; }

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 6px 0 8px;
  padding: 0;
  list-style: none;
  color: var(--text-dim);
  font-size: 11.5px;
}

.chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.chart-legend-item.working { --platform: var(--text); }

.platform-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 3px;
  background: var(--platform, var(--text-faint));
  flex: none;
}

/* The cards: two across on a desktop, so the vendors in use sit beside the
   ones that could be. */
.platform-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 10px;
}

.platform-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}

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

.platform-name {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}

.platform-head .chip { margin-left: auto; }

.platform-blurb,
.platform-rates,
.platform-note {
  margin: 0;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.45;
}

.platform-rates { color: var(--text-faint); font-variant-numeric: tabular-nums; }
.platform-rates-link { color: var(--text-faint); }
.platform-rates-link:hover { color: var(--text); }

/* Four facts in a row, label over value, like the metric tiles but small. */
.platform-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 2px 8px;
  margin: 4px 0 0;
}

.platform-stats dt {
  grid-row: 1;
  color: var(--text-faint);
  font-size: 10.5px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.platform-stats dd {
  grid-row: 2;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Its share of the awake time in the range, as a bar - the fastest read of
   where the time went. */
.platform-share {
  height: 4px;
  border-radius: 2px;
  background: var(--bg-inset);
  overflow: hidden;
}

.platform-share-fill {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: var(--platform, var(--text-faint));
}

.platform-note { color: var(--text-faint); }

/* What went wrong, by name, after acting on several. */
.menu-report { display: grid; gap: 6px; }

/* What has happened to a machine, newest first. */
.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.history-entry {
  display: grid;
  grid-template-columns: 90px 1fr auto;
  gap: 10px;
  align-items: baseline;
  padding: 6px 0;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
}

.history-entry:first-child { border-top: none; }

.history-event { font-weight: 500; }
.history-detail { color: var(--text-dim); min-width: 0; overflow-wrap: anywhere; }
.history-meta { color: var(--text-faint); font-size: 11.5px; white-space: nowrap; }

/* A checkbox with a sentence, in a dialog. */
.modal-check {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 12.5px;
  cursor: pointer;
}

.modal-check input { margin-top: 3px; }

/* A chip that goes somewhere: the repo a machine is in. */
.chip-link {
  background: none;
  font: inherit;
  font-size: 10.5px;
  cursor: pointer;
}

.chip-link:hover { border-color: var(--accent-dim); color: var(--text); }

/* The page is opaque, and has to say so: with the sky behind the whole grid,
   a pane with no ground of its own would be black text on a photograph of
   stars. */
.console-main {
  overflow-y: auto;
  padding: 20px 24px 48px;
  background: var(--bg);
}

/* ------------------------------------------------------------- the detail */

/* One thing's page reads at 760px; a list of things has six columns and is
   given more, or the cells squeeze to three letters each. */
.detail {
  max-width: 760px;
}

.detail.detail-wide {
  max-width: 1080px;
}

.detail-head {
  margin-bottom: 16px;
}

/* A page with one control that belongs beside its name rather than under
   it - the "+" on Connectors. The name and the count line stack on the left
   and the control keeps the top-right corner at every width. */
.detail-head.has-corner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "title corner" "summary corner";
  column-gap: 12px;
  align-items: start;
}

.detail-head.has-corner .detail-title { grid-area: title; }
.detail-head.has-corner .detail-summary { grid-area: summary; }
.detail-head.has-corner .plus-btn { grid-area: corner; }

/* "+", and nothing else: what it adds is what the page is a list of. */
.plus-btn {
  width: 38px;
  height: 38px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 9px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

.plus-btn:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }

/* A button in a row's cell - Connect, on a suggested connector. Kept on a
   phone, where it is the one thing a row still needs to offer. */
.list-cell .list-action {
  padding: 5px 11px;
  font-size: 12px;
}

/* Which secret a new connector uses: one you have, or one typed now. */
.modal-choice {
  display: flex;
  gap: 14px;
}

.modal-choice-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  cursor: pointer;
}

/* A radio, not a text box: the field rule in styles.css pads every input. */
.modal-field .modal-choice-item input {
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  accent-color: var(--accent);
}

/* A choice that cannot be made yet - greyed, and the hint under it says why. */
.modal-choice-item.is-off { color: var(--text-dim); cursor: default; }

/* A button inside a field, after the hint that explains it: "Connect GitHub". */
.modal-inline-btn { align-self: flex-start; margin-top: 6px; }
/* Two such fixes side by side ("Install the App", "Connect GitHub"), wrapping on a phone. */
.modal-inline-actions { display: flex; flex-wrap: wrap; gap: 8px; }
a.modal-inline-btn { text-decoration: none; }

.modal-can {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
}

/* The way back to the list a thing was opened from. At every width: the
   list is not beside the page any more, it is what the page was. */
.detail-back {
  display: inline-block;
  margin-bottom: 12px;
  padding: 3px 8px 3px 4px;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.detail-back:hover { background: var(--bg-hover); color: var(--text); }

.detail-title {
  margin: 0 0 8px;
  font-size: 19px;
  font-weight: 600;
}

/* A title with the control that changes it beside it - the Workspace page's
   Edit, which swaps the name for a field where it is read rather than in a
   settings panel at the foot of the page. The block carries the gap under
   the title so the h1 can give its own up, and any error the rename comes
   back with is appended under the line rather than into it.

   It wraps: on a phone the field and its two buttons take the width, and a
   title too long for one line is not pushed off the side by the button. */
.detail-name {
  margin-bottom: 8px;
}

.detail-title-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.detail-title-line .detail-title { margin-bottom: 0; }

/* `row-edit` takes an auto margin for itself - it is normally the last thing
   on a listing row - and here it belongs against the name, not against the
   far edge of the pane. Scoped the way `.row-head .danger-btn` is. */
.detail-title-line .row-edit { margin-left: 0; }

/* The field takes what is left of the line, down to nothing - without the
   `min-width` a long name would hold the input open past the pane's edge. */
.detail-title-line .focus-form { flex: 1; min-width: 0; }
.detail-title-line .text-input { flex: 1; min-width: 6em; }

.detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

/* ---------------------------------------------------------------- the tabs */

/* Under the repo's name, not in the top bar - see console-repo.js.
   Sticky, so the way to the other parts is still there once the part you are
   on has scrolled; the solid background is what keeps the panels from showing
   through it on the way past. It scrolls sideways rather than wrapping: five
   words wrap into a two-line strip on a phone that reads as two rows of
   buttons, and a strip that runs off the edge reads as a strip. */
.detail-tabs {
  position: sticky;
  /* Sticky measures from the scroller's content box, so 0 would leave the
     pane's top padding as a band the panels scroll through above the strip.
     The pane's padding-top, negated - and again in the phone rules below. */
  top: -20px;
  z-index: 2;
  display: flex;
  gap: 2px;
  margin-top: 4px;
  /* Room below the strip is the strip's to give: what follows it is a panel
     on one page and a row of range chips on another, and only the panel
     brought its own. Collapses with a panel's margin, so nothing doubles. */
  margin-bottom: 14px;
  padding-top: 6px;
  /* An inset shadow rather than a border: the strip scrolls, so anything a
     tab pokes below its padding box is clipped, and a tab's own line has to
     lie on the strip's line, not a pixel above it. */
  box-shadow: inset 0 -1px var(--border);
  background: var(--bg);
  overflow-x: auto;
  scrollbar-width: none;
}

.detail-tabs::-webkit-scrollbar { display: none; }

.detail-tab {
  flex: 0 0 auto;
  padding: 8px 12px 9px;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}

.detail-tab:hover { color: var(--text); }

.detail-tab[aria-selected="true"] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

.detail-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 4px;
}

.agent-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  color: var(--text-dim);
}

.agent-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-faintest);
}

/* Lit when it is awake in either sense; it only pulses when it is busy. */
.agent-status.up .agent-dot { background: var(--accent); }

.agent-status.working .agent-dot {
  animation: rail-pulse 1.6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .agent-status.working .agent-dot { animation: none; }
}

.agent-status.working .agent-status-text { color: var(--text); }

.agent-status-where {
  color: var(--text-faint);
  font-size: 11.5px;
}

.console-panel {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}

.panel-heading {
  margin: 0 0 10px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* A panel that is a whole tab of a page - the Secrets tab of Connectors -
   carries the count line the page head would have carried, beside its
   heading. Wraps rather than truncates on a phone. */
.panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin-bottom: 10px;
}

.panel-head .panel-heading { margin: 0; }
.panel-head .detail-summary { margin: 0; }

/* The Secrets tab: the person's block, then the installation's under a rule. */
.secrets-installation {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

/* A command to copy, as a line on its own. Wraps rather than widens the pane. */
.console-code {
  margin: 6px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  background: var(--bg-deep);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.console-hint {
  margin: 0 0 10px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.55;
}

/* What a page's numbers are and are not - under the table, in small print,
   because "since when" and "counts, not text" are the two questions a
   ranking gets asked and neither belongs in the title. */
/* The session's steering in one sentence, above the tiles that count it:
   "4 turns · 1 cut short · corrected twice · agent 12m, waited 40m". Reads
   as prose, so it is sized as prose and not as a figure. */
.session-autonomy {
  margin: 0 0 10px;
  font-size: 12.5px;
  color: var(--text-faint);
}

/* The same line for the other half of what a session did: what it wrote,
   and what a reviewer kept of it. It wraps, because a phone shows one
   column and the sentence is longer than one. */
.session-written {
  margin: 0 0 10px;
  font-size: 12.5px;
  color: var(--text-faint);
  white-space: normal;
}

.console-caveat {
  margin: 12px 0 0;
  color: var(--text-faint);
  font-size: 11.5px;
  line-height: 1.5;
}

/* ---------------------------------------------------------- home & performance */

/* Home is two sections - what is being worked on, what was just finished -
   each with a heading of its own under the page's, so the eye lands on
   "Working now" and the count beside it before anything else. */
.home-section { margin-top: 18px; }

/* What is waiting on a person, above everything else on Home: a short
   list, title first, with who is waiting and on what under it. */
.home-waiting-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* A card, not a tinted line: the kind of thing it is as a chip beside the
   title, the facts under it, and a row of buttons at the foot set off by a
   rule, because the buttons are what the card is for. The amber is kept
   to the left edge - it still reads as "yours", and the card itself stays
   white so the pull request's chips and the description read as content. */
.home-waiting-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 14px 10px 12px;
  border: 1px solid var(--warn-border);
  border-left: 3px solid var(--warn-text);
  border-radius: 6px;
  background: var(--bg-inset);
  min-width: 0;
}

.home-waiting-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}

.home-waiting-title {
  color: var(--text);
  font-weight: 600;
  font-size: 13px;
  text-decoration: none;
  overflow-wrap: anywhere;
}
.home-waiting-title:hover { text-decoration: underline; }

.home-waiting-note {
  font-size: 11.5px;
  color: var(--text-dim);
}

/* The foot: the primary press first, the links after, and the way to the
   full story pushed to the far end where it is found and not pressed by
   reflex. Wraps on a phone (below) into one button per line. */
.home-waiting-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.home-waiting-actions .task-btn-quiet { margin-left: auto; }
.home-waiting-actions-quiet { border-top: 0; padding-top: 0; margin-top: -4px; }
.home-waiting-actions:empty { display: none; }

/* `hidden` must win over the display these set, or the confirmation shows
   beside the button that is meant to reveal it. */
.home-waiting-confirm[hidden],
.task-resume-btn[hidden] { display: none; }

.home-waiting-merge,
.home-waiting-confirm {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.home-waiting-confirm-text {
  font-size: 12px;
  color: var(--text);
  font-weight: 600;
}

.home-heading {
  margin: 0 0 10px;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
}

/* "Finished recently" is a native <details>, shut to start with: its heading
   is the summary, with a chevron in place of the browser's marker that
   turns down when it is open. */
.home-done > .home-heading {
  list-style: none;
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  gap: 6px;
}

.home-done > .home-heading::-webkit-details-marker { display: none; }

.home-done > .home-heading::before {
  content: "›";
  display: inline-block;
  width: 10px;
  color: var(--text-faint);
  transition: transform 120ms ease;
}

.home-done[open] > .home-heading::before { transform: rotate(90deg); }

/* One card per live session. As many across as fit at a readable width,
   which is two on most desktops and one on a phone (below); a card is
   tall - it has a task, a doing line and a chat box in it - so three
   narrow ones would be worse than two. */
/* A list, one session under the next, never two abreast: a page of cards
   in two columns is read in a zigzag, and the facts on each are the same
   seven, so a column of them reads down like a table. */
.home-cards {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Whose, and where, on the far left; the rest of the card to its right.
   A room of cards is read down that left edge first - who is working, on
   what machine - so it is a column of its own, the same width on every
   card, and the name and the facts start at the same place on each. */
.home-card {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 16px;
  min-width: 0;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
  font-size: 12px;
  color: var(--text-dim);
}

.home-card-side {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 0 0 132px;
  min-width: 0;
  padding-right: 16px;
  border-right: 1px solid var(--border);
}

.home-card-side > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-card-owner {
  color: var(--text);
  font-weight: 600;
  font-size: 13px;
}

.home-card-place { color: var(--text-dim); }
.home-card-host { color: var(--text-faint); font-size: 11px; }

.home-card-main {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
}

.home-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.home-card.live .rail-dot { background: var(--ok); }

/* The name is the way into the session: the card carries facts and links
   out, and everything it dropped - what it is doing, who set it off - is
   one click away here. */
/* The name is a flex item, and a flex item's floor is its own content
   width unless told otherwise: a long name did not shorten, it pushed the
   head - and the card's outline - wider than the page. So it takes the
   room after the dot and no more, and ends in an ellipsis; the whole
   name is the link's hover title. */
.home-card-name {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text);
  font-weight: 600;
  font-size: 13.5px;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-card-name:hover { color: var(--accent); text-decoration: underline; }

/* The facts, label over value, in one row across the card on a desktop -
   the same six in the same order on every card, so the eye reads a column
   of cards down like a table - and two to a row on a phone. A link out -
   the ticket, the task, an open pull request - is a cell after them. */
.home-card-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 24px;
}

.home-fact {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 96px;
  max-width: 100%;
}

.home-fact-label {
  color: var(--text-faint);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.home-fact-value {
  color: var(--text);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-fact-value .session-link,
.home-fact-link { color: var(--text-dim); text-decoration: none; }
.home-fact-value .session-link:hover,
.home-fact-link:hover { color: var(--accent); text-decoration: underline; }
/* The one fact that is a sentence - "#47 since 4m · its review card returns
   when this turn ends" - takes the row and wraps, rather than ending in an
   ellipsis on a phone with the reason cut off. */
.home-fact:has(.home-back-on) { grid-column: 1 / -1; flex-basis: 100%; }
.home-fact-value:has(.home-back-on) { white-space: normal; }

/* Who, on what, because of what: the links under a session's name, on
   the session's page. The names are links; the words between them
   are the faint colour, so the eye lands on the things. */
.session-links {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 0;
  margin: 0;
  color: var(--text-faint);
  font-size: 11.5px;
  line-height: 1.5;
}

.session-link { color: var(--text-dim); text-decoration: none; }
.session-link:hover { color: var(--accent); text-decoration: underline; }
.session-link-text { color: var(--text-dim); }
.session-link-note { color: var(--text-faint); }
.session-link-sep { white-space: pre; }

/* The task it is on: the title, then its state and clock in small print. */
.home-task {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
}

.home-task-title { color: var(--text); }
.home-task-facts { color: var(--text-faint); font-size: 11px; }

/* What it is doing this second, with the same pulsing dot the chat box
   uses. Still, in the faint colour, for an agent between calls: the dot
   pulses only while something is happening. */
.home-doing {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  line-height: 1.5;
}

.home-doing .chat-doing-dot { animation: none; background: var(--text-faint); }
.home-doing.moving .chat-doing-dot { animation: rail-pulse 1.6s ease-in-out infinite; background: var(--accent); }
.home-doing .chat-doing-text { min-width: 0; overflow-wrap: anywhere; }

/* The box on a card: no panel frame inside the card's, a short log, and
   the field one line high - it is for saying one thing about the work
   above it, and the agent's page has the whole conversation. */
.chat-compact {
  margin: 4px 0 0;
  padding: 10px 0 0;
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: none;
}

.chat-compact .console-hint { margin-top: 0; }

.chat-log-short {
  max-height: 180px;
}

/* The chip for a pull request that is still open: the accent, because it
   is the one outcome that is still moving. */
.chip-open {
  border-color: var(--accent-dim);
  color: var(--accent);
}

/* An effective row on the ranking is marked at the edge, not filled: the
   table is read down a column, and a filled row breaks that. On the first
   cell, because a row is `display: contents` and paints nothing itself. */
.perf-table .list-row.effective > .list-open { box-shadow: inset 3px 0 0 var(--ok); }

/* Ten columns of numbers leave a heading no room for a long word, and the
   table cut them off - "Undone afte", "Kept in revie" - which is a column
   nobody can name. Here the headings wrap to a second line instead; the
   cells under them are numbers and still fit on one. */
.perf-table .list-head > * { white-space: normal; }

/* Steering is a few words, and the last column has the room to wrap. */
.activity-table .list-cell:last-child { white-space: normal; }

/* In a detail pane the same table has half the width, so every cell wraps
   rather than losing its tail - "on navigator" is the fact the machine's
   page exists to show. */
.sessions-panel .activity-table .list-cell { white-space: normal; }

/* The comparison above the ranking (console-compare.js): a row of chips
   for what to compare by and a link to flip to the table, a line of
   verdicts, then the charts in a grid that wraps - two or three across on
   a desktop, one on a phone. A chart is a heading, a line saying what the
   figure is, and a bar per option: a name and its task count on the left,
   the bar, the figure at the tip. The bars are thin, sit on a hairline
   baseline, and end square on it and rounded at the tip; the best is gold
   and the rest a grey lighter than text, so the one to look at is the one
   colour on the chart. The panel's paragraphs are gone - the charts are
   the panel now, see console-compare.js - so what is left above them is
   the chips and the way to the table. */
.compare-tools {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.compare-dims { margin: 0; }

/* The way to the numbers, as a button rather than the 12px underlined link
   it was - it is the one other thing on this panel a person does. */
.compare-flip { flex: none; }

.compare-charts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 18px 28px;
}

.chart { min-width: 0; }

.chart-title {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
}

.chart-unit {
  margin: 2px 0 10px;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text-faint);
}

.chart-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.chart-row {
  display: grid;
  grid-template-columns: minmax(96px, 132px) minmax(0, 1fr) 58px;
  align-items: center;
  gap: 10px;
}

.chart-label {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.chart-name {
  font-size: 12.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chart-n {
  font-size: 11px;
  color: var(--text-faint);
}

.chart-track {
  display: block;
  height: 14px;
  border-left: 1px solid var(--border-strong);
}

.chart-bar {
  display: block;
  height: 100%;
  border-radius: 0 4px 4px 0;
  background: var(--bar-rest);
  transition: width 0.3s ease;
}

/* First place, in gold: the one colour on the chart, so the eye finds the
   answer before it reads a number. */
.chart-row.is-best .chart-bar { background: var(--best); }

.chart-value {
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.chart-row.is-best .chart-value { font-weight: 600; }

/* The option the reader picked at the top of the page, in light cyan - a
   fill, and nothing else. It was a fill with a ring, so that a bar which was
   both picked and best could keep the winner's green and take the cyan as an
   outline. Two treatments on one bar to carry two facts is a chart asking to
   be decoded; the facts are already in words beside the figure ("yours",
   "best"), which is where a reader who does not see hues reads them anyway.
   So the pick simply wins the fill: it is the bar you came to find, and the
   green it covers is still named. */
.chart-row.is-picked .chart-bar { background: var(--picked); }

.chart-row.is-picked .chart-name {
  font-weight: 600;
  color: var(--picked-ink);
}

/* The steering panel: one bar per option, stacked by what kind of steer it
   was. It sits among the charts and shares their grid, so what is added
   here is only the stack itself - the segments, their hues, and the legend
   that names them. The hues are the app's own series palette, in the order
   the kinds are stacked, so nothing new enters the page's colour
   vocabulary; the unclassified tail is the same grey a rest-state bar is,
   because it is the absence of a label rather than a seventh kind. */
.steer-track {
  display: flex;
  overflow: hidden;
  border-radius: 0 4px 4px 0;
}

.steer-part {
  display: block;
  height: 100%;
  transition: width 0.3s ease;
}

.steer-clarify { background: var(--series-1); }
.steer-correct { background: var(--series-2); }
.steer-redirect { background: var(--series-3); }
.steer-context { background: var(--series-4); }
.steer-approve { background: var(--series-5); }
.steer-new { background: var(--series-6); }
.steer-unclassified { background: var(--bar-rest); }

/* An outside service's agent is prompted where we cannot see it. Words in
   the track, because a bar of length nought there would read as "never
   needed a word", which is the opposite of what it means. */
.steer-none {
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
}

.steer-legend {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
}

.steer-legend-entry {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--text-faint);
}

.steer-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  flex: none;
}

.steer-buckets { margin: 8px 0 0; }

/* The friction panel: what got in the agents' way, grouped by repository,
   executor or week. A group is a heading and a list of kinds; a kind is a
   name, a bar for the shape of it and its count, with what it means and
   what usually fixes it under. The bar is the app's own warning hue rather
   than a new colour: every row here is something that went wrong, so there
   is nothing to tell apart by colour, only by length. */
.friction-panel .filter-row { margin: 0 0 10px; }

.friction-group {
  padding: 10px 0 4px;
  border-top: 1px solid var(--border);
}

.friction-group:first-of-type { border-top: 0; }

.friction-group-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: 600;
}

.friction-group-n {
  font-size: 11.5px;
  font-weight: 400;
  color: var(--text-faint);
}

.friction-kinds,
.friction-session-kinds {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.friction-kind-line {
  display: grid;
  grid-template-columns: minmax(96px, 132px) minmax(0, 1fr) 42px;
  align-items: center;
  gap: 10px;
}

.friction-kind-name { font-size: 12.5px; }

.friction-track {
  display: block;
  height: 10px;
  min-width: 0;
  border-radius: 4px;
  background: var(--bar-rest);
  overflow: hidden;
}

.friction-bar {
  display: block;
  height: 100%;
  background: var(--err);
  transition: width 0.3s ease;
}

.friction-count {
  font-variant-numeric: tabular-nums;
  font-size: 12.5px;
  text-align: right;
}

/* What the kind means, then the one line of what usually fixes it. The fix
   is the only thing on this panel a person can act on, so it is the darker
   of the two. */
.friction-means,
.friction-fix,
.friction-examples,
.friction-aside {
  margin: 4px 0 0;
  font-size: 11.5px;
  color: var(--text-faint);
}

.friction-fix { color: var(--text-dim); }

.friction-examples { display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline; }
.friction-example { font-size: 11.5px; }
.friction-sep { color: var(--text-faint); }

/* One session's own kinds, on its page: the same words without the bars,
   which compare nothing when there is one row per kind and no second
   group to read them against. */
.friction-session-kind { font-size: 12.5px; }
.friction-session-kind .friction-count { margin-left: 6px; }
.friction-session-kind .friction-means { display: block; }

/* "yours", the same fact in text, for whoever does not read the blue - the
   rule the word "best" already follows below. */
.chart-mine {
  display: block;
  margin-top: 1px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--picked-ink);
}

/* And the word, under the winning figure. The gold is the emphasis and this
   is the same fact in text, for a reader who does not see it as different
   from the greys - a chart whose only answer is a hue has no answer for
   them. Its own darker gold, because the bar's gold on white is a block
   colour and not a text colour. */
.chart-best {
  display: block;
  margin-top: 1px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--best-ink);
}

.compare-table .list-cell { font-variant-numeric: tabular-nums; }

/* The picked option in the table, the same blue as its bar. A left edge and
   a wash rather than a border, so the heat colours in the cells - which are
   the table's own answer - are not competing with a box around them. */
.compare-table .list-row.is-picked {
  background: color-mix(in srgb, var(--picked) 28%, transparent);
  box-shadow: inset 3px 0 0 var(--picked-ink);
}

/* The one filter, at the top of the page (console-facets.js). It is the
   page's first control and it reframes everything under it, so it gets a
   line of its own and a little more air than the range beneath it rather
   than being one dropdown among several. Its own row wraps on a phone the
   way every filter row does, so no breakpoint is needed here. */
.perf-scope {
  margin: 4px 0 2px;
}

.perf-scope .filter-picker {
  flex: 1 1 320px;
  min-width: 0;
}

/* Wider than the console's other dropdowns, because its rows carry two
   things that both have to be read: the name of the thing, and which kind
   of thing it is - which is the only reason somebody who does not know
   Niteshift is a place to run can find it here. Still bounded by the
   viewport, so a phone gets the width it has and no more. */
.perf-scope .filter-picker-pop {
  min-width: min(380px, calc(100vw - 32px));
  max-width: min(460px, calc(100vw - 32px));
}

/* A skill's row on the Tools page says what loading it did - "first time
   88% with it, 38% without" - after the three words every other row's note
   holds. A note is one ellipsised line everywhere else, and at every width
   that cut this one off mid-word: the numbers are at the end and they are
   the only part worth having. So notes on this table wrap, at both widths,
   and the rows are a little taller for it. */
.tools-table .list-note {
  white-space: normal;
  overflow: visible;
}

/* ---------------------------------------------------- harness changes

   One card per commit that changed how the agents work here (CLAUDE.md, a
   skill, a hook), each with the fortnight either side of it. The cards are
   a grid that becomes one column when there is not room for two - no
   breakpoint decides it, `auto-fill` does, which is what keeps this
   readable at 390px without a rule of its own. */
.harness-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 12px;
}

.harness-card {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  min-width: 0;
}

.harness-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
}

/* The commit's own subject line, which is the name of the change. It wraps
   rather than being cut off: a rule nobody can read the name of is a card
   nobody can act on. */
.harness-card-title {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.harness-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-top: 6px;
  font-size: 11.5px;
  color: var(--text-dim);
}

.harness-files { overflow-wrap: anywhere; }

.harness-verdict {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin: 10px 0 4px;
}

.harness-sentence { flex: 1 1 200px; }

/* What the two sides stand on, said in plain words and never behind a
   hover: "71% vs 54%" on four sessions each is not a finding, and a reader
   who has to hover to learn that will not. */
.harness-counts {
  margin: 0;
  font-size: 11.5px;
  color: var(--text-dim);
}

/* The figures that moved, before → after. A two-column list on a desktop
   and, below 860px, the label over its pair - see the phone block. */
.harness-figures {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 3px 12px;
  margin: 10px 0 0;
  font-size: 12px;
}

.harness-figures dt {
  color: var(--text-dim);
  min-width: 0;
  overflow-wrap: anywhere;
}

.harness-figures dd {
  margin: 0;
  display: flex;
  gap: 5px;
  align-items: baseline;
  white-space: nowrap;
}

.harness-was { color: var(--text-dim); }
.harness-arrow { color: var(--text-dim); }
.harness-now { font-weight: 600; }
.harness-good .harness-now { color: var(--ok-text); }
.harness-bad .harness-now { color: var(--err-text); }

.harness-beside {
  margin-top: 10px;
  font-size: 11.5px;
}

.harness-beside-head {
  cursor: pointer;
  color: var(--text-dim);
}

.harness-beside-row {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  margin-top: 6px;
}

.harness-beside-name {
  font-weight: 600;
  flex: 0 0 auto;
}

.harness-beside-side {
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/* The ranking's own heading, under the comparison's panel. */
.perf-ranking-heading { margin: 24px 0 10px; }

/* The box beside the ranking - the one thing the filter at the top cannot
   offer, being words rather than the name of anything. `.list-tools` stacks
   its children, which left the right half of the row empty, so this
   overrides it back to a row; it wraps rather than shrinks, so on a phone
   the box takes the width it needs. No breakpoint decides that, the wrap
   does. */
.perf-filters {
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
}

.perf-filters .list-search {
  flex: 1 1 260px;
  min-width: 0;
  max-width: 420px;
}

/* The row of dropdowns is a row inside a row now, so its own bottom margin
   would push the box it sits beside off centre. */
.perf-filters .filter-row {
  flex: 0 1 auto;
  margin-bottom: 0;
}

/* Taking the page away as a file (console-performance.js `exportPicker`).
   It sits in the range's row and borrows `.filter-picker` for the label
   beside it, so it lines up with the dropdowns and inherits the phone rule
   that stacks them; what is its own is the group of four buttons, which
   wraps rather than shrinks, and the line under it that says what was
   saved or why nothing was. */
/* The picker wraps where a dropdown does not: what was saved goes on a line
   of its own under the buttons rather than stealing their width. */
.export-picker { flex-wrap: wrap; }

.export-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-width: 0;
}

.export-btn { white-space: nowrap; }

.export-said {
  flex: 1 1 100%;
  color: var(--text-faint);
  font-size: 11.5px;
}

/* Nothing said yet is no line at all, not a gap under the buttons. */
.export-said:empty { display: none; }

.export-said.is-problem { color: var(--err-text); }

/* Over time: one metric as a line (console-trend.js). The plot is an SVG
   stretched to the box with a non-scaling stroke; everything with text in
   it is HTML placed by percentage, so it fits a phone without shrinking. */
.trend { margin-top: 16px; }
.trend-metrics { margin: 0; }
.trend-flip { font-size: 12px; }

.trend-said {
  margin: 0 0 4px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-dim);
}

.trend-chart {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  grid-template-rows: 180px 22px;
  column-gap: 8px;
  margin: 14px 0 6px;
}

.trend-axis {
  position: relative;
  grid-row: 1;
}

.trend-axis-tick {
  position: absolute;
  right: 0;
  transform: translateY(-50%);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}

.trend-plot {
  position: relative;
  grid-row: 1;
  border-left: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border-strong);
  touch-action: pan-y;
}

.trend-grid {
  position: absolute;
  left: 0;
  right: 0;
  border-top: 1px dashed var(--border);
  pointer-events: none;
}

.trend-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.trend-area { fill: var(--accent-dim); fill-opacity: 0.35; }

.trend-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2px;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* A marker per point: 8px in the accent with a 2px ring of the surface,
   and a hit target well past its own size. */
.trend-dot {
  position: absolute;
  width: 8px;
  height: 8px;
  padding: 0;
  margin: -4px 0 0 -4px;
  border: 2px solid var(--bg);
  border-radius: 50%;
  background: var(--accent);
  box-sizing: content-box;
  cursor: default;
}

.trend-dot::before {
  content: "";
  position: absolute;
  inset: -10px;
}

.trend-dot.is-peak { background: var(--ok); }
.trend-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.trend-hair {
  position: absolute;
  top: 0;
  bottom: 0;
  border-left: 1px solid var(--text-faintest);
  pointer-events: none;
}

.trend-tip {
  position: absolute;
  top: 6px;
  transform: translateX(8px);
  padding: 4px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-raised);
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
  z-index: 1;
}

.trend-tip.is-left { transform: translateX(calc(-100% - 8px)); }
.trend-tip strong { font-variant-numeric: tabular-nums; }
.trend-tip-when { display: block; margin-bottom: 2px; color: var(--text-faint); }
.trend-tip-row { display: flex; align-items: center; gap: 6px; }
.trend-tip-name { color: var(--text-dim); }

/* Split: a line per group in the slot's hue - the same six, in the same
   order, on the line, the swatch and the name at the line's end. Text
   keeps its own ink; only the mark and the swatch carry the hue. */
.trend-line.series-1 { stroke: var(--series-1); }
.trend-line.series-2 { stroke: var(--series-2); }
.trend-line.series-3 { stroke: var(--series-3); }
.trend-line.series-4 { stroke: var(--series-4); }
.trend-line.series-5 { stroke: var(--series-5); }
.trend-line.series-6 { stroke: var(--series-6); }

.trend-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--bg);
  box-sizing: content-box;
  flex: none;
}

.trend-swatch.series-1 { background: var(--series-1); }
.trend-swatch.series-2 { background: var(--series-2); }
.trend-swatch.series-3 { background: var(--series-3); }
.trend-swatch.series-4 { background: var(--series-4); }
.trend-swatch.series-5 { background: var(--series-5); }
.trend-swatch.series-6 { background: var(--series-6); }

.trend-end {
  position: absolute;
  right: 0;
  transform: translate(calc(100% + 6px), -50%);
  font-size: 11px;
  white-space: nowrap;
  color: var(--text-dim);
  pointer-events: none;
}

/* Room at the right for the names at the lines' ends. */
.trend-chart.is-split .trend-plot,
.trend-chart.is-split .trend-labels { margin-right: 96px; }

.trend-legend {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  font-size: 12px;
}

.trend-legend-item { display: flex; align-items: center; gap: 6px; }
.trend-legend-total { color: var(--text-faint); font-variant-numeric: tabular-nums; }

.trend-splits { margin: 0 0 10px; align-items: center; }
.trend-splits-label { font-size: 12px; color: var(--text-faint); margin-right: 4px; }

.trend-labels {
  position: relative;
  grid-column: 2;
  grid-row: 2;
}

.trend-label {
  position: absolute;
  top: 4px;
  transform: translateX(-50%);
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
}

.trend-label:first-child { transform: none; }
.trend-label:last-child:not(:first-child) { transform: translateX(-100%); }

.trend-table .list-cell { font-variant-numeric: tabular-nums; }

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

/* The Tools page: three sections - tools, connectors, skills - each a
   heading, a line about it and a table. What kind of tool the calls go to
   is one quiet line above the tools table; a skill has no page, so its row
   leads nowhere and must not look like a link. A tool's own page lists who
   calls it and what it was used on, each a line with a link, a chip and a
   note, in that order. */
.tools-section { margin-top: 18px; }
.tools-section .panel-heading { margin-bottom: 4px; }

/* A connector, a skill, a tool: one row each, in the shared list table.
   Nothing here but the width - five numbers is more columns than a list
   usually carries, so the name takes less of the row than the 2.4fr a
   list of machines gives it. */
.tools-table { grid-template-columns: minmax(0, 1.8fr) repeat(var(--cells), minmax(0, 1fr)); }

/* The sessions a tool was in, on the tool's own page: the last few by
   name, each with what it came to. */
.tool-sessions-block { margin-top: 10px; }

.tool-sessions-title {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.tool-sessions {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.tool-session {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  font-size: 12.5px;
}

.tool-session-name {
  color: var(--text);
  font-weight: 500;
  text-decoration: none;
}

.tool-session-name:hover { color: var(--accent); text-decoration: underline; }

.tool-session-note {
  color: var(--text-faint);
  font-size: 11.5px;
}

.tool-sessions-more {
  margin: 6px 0 0;
  color: var(--text-faint);
  font-size: 11.5px;
}

.tools-agents,
.tools-tasks { list-style: none; margin: 10px 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }

.tools-agent,
.tools-task { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13px; }

.tools-agent-name,
.tools-task-title { color: var(--text); font-weight: 500; text-decoration: none; }

.tools-agent-name:hover,
.tools-task-title:hover { color: var(--accent); text-decoration: underline; }

.tools-agent-note,
.tools-task-note { color: var(--text-faint); font-size: 12px; }

/* One session's timeline: a block per trace, a line per span. Tabular
   numerals so the times and durations line up down the column. */
.timeline { display: flex; flex-direction: column; gap: 12px; }

.trace {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.trace-head {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 10px;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
  font-size: 11px;
  color: var(--text-faint);
}

.trace-name { color: var(--text-dim); font-weight: 600; }

.timeline-row {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  padding: 5px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.timeline-row:last-child { border-bottom: none; }
.timeline-row.failed { background: var(--err-bg); }
.timeline-time { color: var(--text-faint); font-size: 11px; }
.timeline-text { display: flex; flex-direction: column; min-width: 0; }
.timeline-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.timeline-note { color: var(--text-faint); font-size: 11px; overflow-wrap: anywhere; }
.timeline-took { color: var(--text-dim); font-size: 11px; white-space: nowrap; }

/* The stop, and the word on why there is none: one line, the button
   first. The note wraps under it on a phone. */
.steer-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0;
  font-size: 12px;
}

/* First on its line, not pushed to the edge the way a row's delete is;
   framed, since it is the one thing here that does something. */
.steer-stop { margin-left: 0; border-color: var(--border-strong); color: var(--text-dim); }
.steer-stop:disabled { cursor: default; color: var(--text-faintest); border-color: var(--border); }

.steer-note { color: var(--text-faint); min-width: 0; }

/* The transcript: a scrolling box of rows on the timeline's grid - the
   clock, the words, then how long and whether it worked for a tool call.
   Tall enough to read a turn, short enough that the timeline under it is
   still on the page; the reader's place in it is kept across redraws. */
.transcript {
  display: flex;
  flex-direction: column;
  max-height: 440px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}

.transcript > .console-hint { margin: 8px 10px; }
.transcript-more { text-align: center; }

.transcript-row {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr) auto;
  align-items: start;
  gap: 10px;
  padding: 5px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.transcript-row:last-child { border-bottom: none; }
.transcript-row.said { background: var(--bg); }
.transcript-row.person { background: var(--accent-wash); }
.transcript-row.tool.failed { background: var(--err-bg); }
.transcript-row.thought { color: var(--text-faint); font-style: italic; }
.transcript-time { color: var(--text-faint); font-size: 11px; padding-top: 1px; }
.transcript-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.transcript-words { color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
.transcript-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.transcript-note { color: var(--text-faint); font-size: 11px; overflow-wrap: anywhere; }
/* A subagent's rows: indented under a rule, its name first, at both widths. */
.transcript-row.sub .transcript-text { padding-left: 12px; border-left: 2px solid var(--line); }
.transcript-agent { color: var(--text-faint); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; margin-right: 6px; }
.transcript-mark { color: var(--text-dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
.transcript-row.mark.stopped .transcript-mark,
.transcript-row.mark.failed .transcript-mark { color: var(--err-text); }
.transcript-tail { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.transcript-took { color: var(--text-dim); font-size: 11px; }

/* A session's links out - its pull requests, the Slack threads it spoke
   into: a chip for where a thing stands, a link, a note. Wraps, because at
   phone width the link and its note do not fit on one line. */
.pull-lines { display: flex; flex-direction: column; gap: 6px; }

.pull-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 12px;
}

.pull-link { color: var(--text); }
.pull-note { color: var(--text-faint); font-size: 11px; }

/* What became of a merge afterwards: reverted, followed by a fix, red on
   the branch, in production, still there a month on. Indented under the
   pull request it is about, and wrapping like the line above it - at phone
   width "Reverted" and the link to the revert do not sit side by side. */
.pull-line-group { display: flex; flex-direction: column; gap: 4px; }

.pull-after {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-left: 10px;
  padding-left: 9px;
  border-left: 1px solid var(--border);
}

.pull-aftermath {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 11px;
}

.pull-aftermath-what { color: var(--text-dim); }

/* ---- pull-cost ----
   Where a change's money went: one bar of three bands, before review,
   answering the review, and after it was over. The bands are sized by
   flex-grow rather than by width, so three per cent still draws something
   rather than collapsing to nothing, and the legend under it carries the
   names - a band narrow enough to be interesting is too narrow to label,
   and at phone width most of them are. */
.phase-where { margin-bottom: 12px; }

.phase-where-head {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 6px;
  font-size: 11.5px;
  color: var(--text-dim);
}

.phase-bar {
  display: flex;
  gap: 2px;
  height: 22px;
  overflow: hidden;
}

.phase-band {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 3px;
  border-radius: 3px;
  overflow: hidden;
  font-size: 10.5px;
  /* The bands are the chart hues, which are dark enough to carry the
     palette's light ink - the same pairing a filled accent button uses. */
  color: var(--on-accent);
}

.phase-band-share { padding: 0 2px; }

.phase-band.phase-before, .phase-swatch.phase-before { background: var(--series-1); }
.phase-band.phase-review, .phase-swatch.phase-review { background: var(--series-2); }
.phase-band.phase-after, .phase-swatch.phase-after { background: var(--series-3); }

.phase-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin-top: 7px;
  font-size: 11.5px;
}

.phase-legend-entry { display: flex; align-items: center; gap: 5px; }

.phase-swatch {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex: none;
}

.phase-legend-label { color: var(--text-dim); }
.phase-legend-value { color: var(--text); }

/* Adoption: the four rungs, borrowing the phase bar above rather than
   keeping a second bar of its own - the phone rules that drop a narrow
   band's number already apply, and a second bar would be a second set of
   them to keep true. The rungs run cool to warm because they are an
   order, not a set of options: rung one is not a failing colour. */
.phase-band.level-1, .phase-swatch.level-1 { background: var(--series-1); }
.phase-band.level-2, .phase-swatch.level-2 { background: var(--series-3); }
.phase-band.level-3, .phase-swatch.level-3 { background: var(--series-4); }
.phase-band.level-4, .phase-swatch.level-4 { background: var(--series-2); }

.adoption-strip { margin-top: 12px; }

/* The rung, as a chip on the row, in the same four colours as the band it
   is counted in - so a person's row and the strip above it read as one
   thing. White ink, because these are the filled colours. */
.chip.level-chip { color: var(--on-accent); border-color: transparent; }
.chip.level-chip.level-1 { background: var(--series-1); }
.chip.level-chip.level-2 { background: var(--series-3); }
.chip.level-chip.level-3 { background: var(--series-4); }
.chip.level-chip.level-4 { background: var(--series-2); }

.adoption-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

/* The sentence that put somebody on their rung. Under the name and not
   behind a hover: a phone has no hover, and a level with no reason beside
   it is a badge nobody can argue with (server/adoption.js).

   It wraps at both widths and not only on a phone. The name column of a
   six-column table is narrow, and the desktop's ellipsis cut the sentence
   off mid-path - which leaves a rung with half a reason, the one thing
   this must not do. Same fix, and the same reason, as .tools-table's note. */
.adoption-reason {
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--text-dim);
  white-space: normal;
}

.adoption-next { margin-top: 10px; }
.adoption-next strong { color: var(--text); }

/* A pull request's own bar, on the session page under the line about it.
   Thinner, no numbers in the bands - the line beside it says the total. */
.pull-phases {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 11px;
}

.pull-phases .phase-bar {
  height: 8px;
  flex: 1 1 120px;
  min-width: 90px;
}

.pull-phases .phase-band-share { display: none; }
.pull-phase-total { color: var(--text-dim); }
/* The phone rules for these live in the one 860px block at the foot of the
   file - a second block earlier up would be a second breakpoint to keep
   true, and test/console.test.js refuses one. */
/* ---- end pull-cost ---- */

/* ---- merge-blame ----
   Which lines of a merge an agent wrote: the panel under a pull request's
   line on the session page (public/console-home.js `blameView`).

   It is code, so it is set in the mono face and does not wrap - a wrapped
   line of code reads as two lines and puts every number after it out. The
   consequence is that a long line has to go somewhere, and it goes sideways
   inside `.blame-code`, which is the one box on the page that scrolls: the
   column itself must not, at any width.

   The gutters are 3px of colour and carry no text, so nothing here rests on
   hue alone - the legend above names every session, and the file's own head
   says the share in words and numbers. */
.blame-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 6px 0 2px 10px;
  padding-left: 9px;
  border-left: 1px solid var(--border);
}

.blame-open, .blame-more {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

.blame-more { padding: 6px 8px; font-size: 11px; }

.blame-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 14px;
  font-size: 11px;
}

.blame-key { display: inline-flex; align-items: center; gap: 5px; }
.blame-key-name { color: var(--text); }

.blame-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
}

.blame-note { font-size: 11px; color: var(--text-faint); }

.blame-file {
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}

.blame-file-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  font-size: 11.5px;
}

.blame-path { font-family: var(--font-mono); color: var(--text); overflow-wrap: anywhere; }

.blame-share {
  flex: 0 1 120px;
  min-width: 60px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}

.blame-share-fill { display: block; height: 100%; background: var(--accent); }

/* The one box on this page that scrolls sideways, so the column never has to. */
.blame-code {
  max-height: 440px;
  overflow-x: auto;
  overflow-y: auto;
  padding: 4px 0;
}

.blame-row {
  display: flex;
  align-items: stretch;
  gap: 6px;
  min-width: max-content;
}

.blame-gutter, .blame-gitai { flex: 0 0 3px; border-radius: 1px; }
.blame-gitai { background: transparent; }
.blame-gitai-agent { background: var(--accent-dim); }
.blame-gitai-human { background: var(--text-faint); }

.blame-n {
  flex: 0 0 auto;
  min-width: 34px;
  padding-left: 2px;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 11px;
  text-align: right;
  user-select: none;
}

.blame-text {
  padding-right: 8px;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 11.5px;
  white-space: pre;
}

/* One hue per session, the app's own series palette in the order the server
   listed them; past six they share a colour and the legend tells them apart.
   A line no session here wrote gets the neutral, which says "no match" and
   not "a person wrote this". */
.blame-by-1 { background: var(--series-1); }
.blame-by-2 { background: var(--series-2); }
.blame-by-3 { background: var(--series-3); }
.blame-by-4 { background: var(--series-4); }
.blame-by-5 { background: var(--series-5); }
.blame-by-6 { background: var(--series-6); }
.blame-by-other { background: var(--text-dim); }
.blame-by-none { background: var(--border); }
/* The phone rules for these live in the one 860px block at the foot of the
   file - see the note under the pull-cost bar above. */
/* ---- end merge-blame ---- */

.console-problem {
  margin: 8px 0 0;
  padding: 7px 10px;
  border: 1px solid var(--err-border);
  border-radius: 6px;
  background: var(--err-bg);
  color: var(--err-text);
  font-size: 12px;
}

/* It worked, said where the failure would have been said. Same box, the
   palette's ok colours: a form that clears its error and shows nothing
   looks like a form that did nothing. */
.console-done {
  margin: 8px 0 0;
  padding: 7px 10px;
  border: 1px solid var(--ok-border);
  border-radius: 6px;
  background: var(--ok-bg);
  color: var(--ok-text);
  font-size: 12px;
}

/* ------------------------------------------------------------ what it did */

.call-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.call {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 6px 0;
  border-top: 1px solid var(--border);
}

.call:first-child { border-top: none; }

.call-mark {
  flex: 0 0 auto;
  width: 12px;
  margin-top: 1px;
  text-align: center;
  color: var(--text-faintest);
  font-size: 11px;
}

.call-running .call-mark { color: var(--accent); }
.call-failed .call-mark { color: var(--err); }

.call-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.call-summary {
  /* Commands and paths, which is most of what these are. */
  font-family: var(--font-mono);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.call-failed .call-summary { color: var(--err-text); }

.call-where,
.call-detail {
  color: var(--text-faint);
  font-size: 11px;
}

.call-detail { color: var(--err-text); }

/* ----------------------------------------------------------------- rows */

.row-stacked,
.row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 7px 0;
  border-top: 1px solid var(--border);
}

.row-stacked:first-child,
.row:first-child { border-top: none; }

.row-name {
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
}

.row-name:hover { color: var(--accent); text-decoration: underline; }

/* The same look as the anchor it replaced - it goes to a page, it just happens
   to be a page in this document. */
.row-name-btn {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

/* The end of a row, whatever is there: the workspace switcher's Switch, and
   the chip that stands in its place on the one already open. `row-edit` and
   `danger-btn` take the same margin for themselves; this is for the things
   that belong at the end without also being quiet. */
.row-end { margin-left: auto; }

/* Chips, wherever they sit: under a page title, or on a grant row. This was
   `.app-status` as well, declared twice in this file with two different gaps -
   the second quietly won, which is the whole argument for one name. */
.row-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.chip {
  padding: 1px 7px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text-dim);
  font-size: 10.5px;
  white-space: nowrap;
}

.chip-none { color: var(--text-faintest); }

/* A row that is not this workspace's own repo says so (console-where.js).
   Outlined, not filled: it is a fact about where the work was, not a
   warning about it - amber would read as "something is wrong with this
   session", and nothing is. Dashed, so it is still distinct from the
   plain chips it sits among at a glance. */
.chip-where {
  border-style: dashed;
  border-color: var(--border-strong);
  color: var(--text-dim);
}

/* The Repo fact on a work card, and the Where cell on the Activity table:
   the name and its mark on one line, wrapping together when the column is
   narrow rather than the mark dropping to a line of its own. */
.home-fact-where,
.activity-where {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
}

/* A session's name on the Performance table, with its mark beside it. */
.perf-row-name {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px;
}

.chip-warn {
  border-color: var(--warn-border);
  background: var(--warn-bg);
  color: var(--warn-text);
}

.chip-ok {
  border-color: var(--ok-border);
  background: var(--ok-bg);
  color: var(--ok-text);
}

/* Something is stopping it. Red, not amber: amber is "wait", and nothing
   about a refused model call gets better by waiting. */
.chip-err {
  border-color: var(--err-border);
  background: var(--err-bg);
  color: var(--err-text);
}

/* A grant is a row that can open: the name and what it may do, and under it
   the checkboxes when somebody is changing them. A modal for six checkboxes,
   over a list of six rows, is a modal you open six times. */
.row-stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.row-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.row-body:not(:empty) {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg-inset);
}

.row-edit-actions {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

/* The two setups' switch. It sits beside the heading on a desktop and wraps
   under it on a phone, where flush against the first line of the view it
   reads as a heading rather than as a control. */
.setup-head {
  flex-wrap: wrap;
  row-gap: 6px;
  margin-bottom: 10px;
}

/* What holds of a whole panel rather than of the last thing in it: set off
   by a rule, so a caveat about the panel is not read as one more sentence
   of the step above it. */
.panel-foot {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.panel-foot .console-hint:first-child { margin-top: 0; }

/* A link that goes somewhere - the LiteLLM proxy's own admin UI - sitting in
   a row of buttons. It needs the reset `button` gets for free in styles.css,
   and it wraps rather than pushing the panel wide on a phone. */
.row-edit-actions a.ghost-btn {
  display: inline-block;
  text-decoration: none;
  line-height: 1.2;
  cursor: pointer;
}

.row-edit-actions { flex-wrap: wrap; }

.row-add {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}

.row-add-label {
  color: var(--text-faint);
  font-size: 11.5px;
}

/* The "Start the team" row: a button and a sentence, which on a phone is
   a button and then a sentence. */
.row-wake {
  flex-wrap: wrap;
}

.row-edit {
  margin-left: auto;
  border: 1px solid transparent;
  background: none;
  color: var(--text-faint);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
  padding: 2px 8px;
  border-radius: 5px;
}

.row-edit:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

/* `row-edit` already took the auto margin, so this one only needs to sit
   beside it rather than pushing itself to the end. */
.row-head .danger-btn {
  margin-left: 0;
}

.focus-form {
  display: flex;
  gap: 8px;
}

/* Connecting by signing in (console-connectors.js connect): the button is
   the panel, and the token way is folded under it - a native <details>,
   whose summary reads as the small link it is, not as a box. */
.connect-other { margin-top: 12px; }
.connect-other summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
  font-size: 11.5px;
  color: var(--text-dim);
}
.connect-other summary::-webkit-details-marker { display: none; }
.connect-other summary:hover { color: var(--accent); text-decoration: underline; }
.connect-other[open] summary { margin-bottom: 8px; }
.connect-with-secret { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.connect-with-secret .focus-form { align-self: stretch; }
.panel-subheading {
  margin: 14px 0 4px;
  font-size: 12px;
  font-weight: 600;
}

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

/* A box with the list under it. The list is anchored to the picker, not the
   page, and spans exactly the picker's width - so in a 390px column it is
   the column's width, and in a row it is the box's. */
.model-picker {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
  min-width: 0;
}

.model-box { position: relative; display: flex; }

.model-input { width: 100%; box-sizing: border-box; }

.model-picker .model-menu {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  z-index: 6;
  max-height: 280px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--bg-raised);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  display: flex;
  flex-direction: column;
}

/* `display: flex` above out-specifies the browser's own `[hidden]`, so the
   closed list would sit as a 10px bar across the note. Same fix as
   .template-form[hidden]. */
.model-picker .model-menu[hidden] { display: none; }

.model-group {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 10px 3px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.model-warn {
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--err);
}

.model-option {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 10px;
  align-items: baseline;
}

.model-option.is-active,
.model-option.is-current { background: var(--bg-hover); }

.model-option-id {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

.model-option-note {
  grid-column: 1 / -1;
  font-size: 11px;
  color: var(--text-faint);
}

.model-empty {
  padding: 8px 10px;
  font-size: 12px;
  color: var(--text-faint);
}

.model-note.is-warn { color: var(--err); }

/* The row form: the picker takes the width, the button sits at the top of
   it, level with the box rather than centred on the note under it. */
.model-form { align-items: flex-start; }

.model-said:not(:empty) { margin-top: 8px; }

.model-row .restart-check { margin-top: 10px; }

/* ---------------------------------------------------------- harnesses */

/* The harness select on the agent's page, above the model: a labelled
   select and its Save in one row, the way the model form is laid out. */
.harness-form { align-items: flex-end; margin-bottom: 10px; }
.harness-form .modal-field { flex: 1; min-width: 0; }
.harness-form .model-said { flex-basis: 100%; }

/* A harness row on the Account page: name, chips, and its buttons pushed
   to the end. The note, when there is one, takes the whole line under. */
.harness-row .row-edit-actions { margin-left: auto; }
.harness-row .row-note { flex-basis: 100%; }
.harness-row .row-body:not(:empty) { flex-basis: 100%; }
.harness-panel .row-edit-actions { margin-top: 8px; }

/* A snippet of several lines - an env block, a hook's JSON - keeps its
   lines and scrolls sideways inside its box rather than the page.

   Both classes in the selector on purpose: `.credential` sets
   `align-items: center` further down this file, and at equal specificity
   the later rule wins - which left the Copy button floating halfway down a
   forty-line settings file, a long way from the thing it copies. */
.credential.credential-block { align-items: flex-start; }
.credential-block .credential-value { white-space: pre; max-height: 260px; overflow: auto; }

/* ------------------------------------------------- connect your own setup */

/* The setup line is the top of the Executors page (console-connect.js
   setupPanel), and it is a hundred and twenty characters with the token
   in it: it wraps, so the whole of it can be seen, rather than scrolling
   off behind the Copy button. */
.connect-steps .credential-value { white-space: pre-wrap; word-break: break-all; }
.connect-steps .connect-step .cap-hint { line-height: 1.5; }
.connect-script { margin-top: 4px; }

/* ----------------------------------------------------- external agents */

/* Somebody else's agent's page. A configuration file is a box of text
   under the repository line that reads it; a vendor's tool is a closed
   row that opens into its form, so twenty tools are twenty lines and not
   twenty forms; what a tool answered keeps its lines and scrolls in its
   own box. */
.external-config-form { margin-bottom: 8px; }
.external-config-editor { display: flex; flex-direction: column; gap: 8px; }
.external-config-text {
  width: 100%;
  min-height: 180px;
  resize: vertical;
  line-height: 1.45;
  font-family: var(--font-mono);
  font-size: 11.5px;
  box-sizing: border-box;
}
.external-json { min-height: 60px; resize: vertical; font-family: var(--font-mono); font-size: 11.5px; }
.external-tool {
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg-inset);
  margin-bottom: 6px;
}
.external-tool-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  cursor: pointer;
  list-style: none;
}
.external-tool-head::-webkit-details-marker { display: none; }
.external-tool-head .row-name { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: 12px; }
/* The name opens the details under it, it is not a link to a page. */
.external-tool-head .row-name:hover { color: var(--text); text-decoration: none; }
/* A boolean argument: the box sits at the left edge like the inputs above it,
   without the text-input padding the modal gives every input. */
.external-tool-form input[type="checkbox"] {
  align-self: flex-start;
  width: 16px;
  height: 16px;
  padding: 0;
  margin: 2px 0 0;
  accent-color: var(--accent);
}
.external-tool-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 10px 10px;
  border-top: 1px solid var(--border);
}
.external-tool-form { padding-top: 8px; }
.external-answer {
  margin: 0;
  padding: 8px 10px;
  border-radius: 5px;
  background: var(--bg);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 320px;
  overflow: auto;
}
.external-dispatch .focus-form { margin: 0; }
.external-url {
  font-family: var(--font-mono);
  font-size: 11.5px;
  word-break: break-all;
}

.provider-keys code {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 1px 4px;
  border-radius: 3px;
  background: var(--bg-inset);
}

/* ---------------------------------------------------- work between agents */

.task-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.task {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 7px 0;
  border-top: 1px solid var(--border);
}

.task:first-child { border-top: none; }

.task-mark {
  flex: 0 0 auto;
  width: 12px;
  margin-top: 1px;
  text-align: center;
  font-size: 11px;
  color: var(--text-faintest);
}

/* Only the states where somebody is waiting get a colour. Done and declined
   are settled, and a settled row is scenery. */
.task-open .task-mark { color: var(--accent); }
.task-accepted .task-mark { color: var(--ok); }
.task-blocked .task-mark { color: var(--warn-text); }

.task-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.task-title { font-weight: 500; }

.task-done .task-title,
.task-declined .task-title {
  color: var(--text-dim);
}

/* A failure is not scenery: it is a decision somebody owes. */
.task-failed .task-mark { color: var(--err); }
.task-failed .task-owner { color: var(--err-text); }

.task-where {
  color: var(--text-faint);
  font-size: 11px;
}

.task-detail,
.task-note {
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.5;
}

.task-note { color: var(--text-faint); font-style: italic; }

/* Who has it - the first thing asked of a task, so it is the first line
   under the title and reads as a sentence, not a state code. */
.task-owner {
  font-size: 12px;
  color: var(--text-dim);
}

.task-accepted .task-owner { color: var(--ok); }
.task-blocked .task-owner { color: var(--warn-text); }

/* A task waiting on a person - see console-tasks.js waitingBlock. The ask
   is a line and one control, set in the warn tint so it reads as "yours",
   not as "broken": the one thing on the board that wants something from
   the reader. The note and the button sit side by side and wrap under each
   other when there is no room (a phone, below). */
.task-waiting {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 4px 0 2px;
  padding: 8px 10px;
  border: 1px solid var(--warn-text);
  border-radius: 6px;
  background: var(--warn-bg);
}

.task-waiting-line {
  font-size: 12px;
  color: var(--warn-text);
  font-weight: 600;
}

.task-waiting-link { color: inherit; text-decoration: underline; }

.task-resume {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.task-resume-note {
  flex: 1;
  min-width: 160px;
  padding: 4px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

/* The buttons on a waiting task, one family in three weights. The press
   that moves the task - Answer, Resume, Approve, Merge - is solid in
   orange (--attention), not the accent: every other button on the page is
   green, and this is the one that is somebody's move, so it is the one
   colour nothing else on the page uses and is found without reading. The
   ones that go somewhere else (GitHub, a tab) are ghosts with a border; the
   way to the full story is quiet text. Deny is a ghost in the error
   colour: visible, never the one pressed by reflex. A link with the class
   is a button to the eye and a link to the browser. */
.task-resume-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 5px 14px;
  border: 1px solid var(--attention);
  border-radius: 6px;
  background: var(--attention);
  color: var(--on-attention);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.3;
  text-decoration: none;
  cursor: pointer;
  min-height: 30px;
  white-space: nowrap;
}
.task-resume-btn:hover { filter: brightness(1.08); }
.task-resume-btn:disabled { opacity: 0.5; cursor: default; filter: none; }

.task-resume-btn.task-btn-ghost {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text);
}
.task-resume-btn.task-btn-ghost:hover { background: var(--bg-hover); filter: none; }

.task-resume-btn.task-btn-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--text-dim);
  font-weight: 500;
  padding-left: 6px;
  padding-right: 6px;
}
.task-resume-btn.task-btn-quiet:hover { color: var(--text); text-decoration: underline; filter: none; }

.task-resume-error {
  font-size: 11.5px;
  color: var(--err-text);
}

/* The decision on an access request (console-tasks.js decisionBlock): a
   line for the agent, then three approvals of different lengths and a
   denial in one row. The lengths are the decision, so they are buttons and
   not a picker; Deny is set apart in the error colour so it is never the
   one pressed by reflex. */
.task-decide {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.task-decide-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.task-decide-deny {
  background: transparent;
  border-color: var(--err-border);
  color: var(--err-text);
  margin-left: auto;
}
.task-decide-deny:hover { background: var(--err-bg); filter: none; }

.task-brief-request {
  font-size: 12px;
  color: var(--text);
}
.task-brief-request-sensitive strong { color: var(--warn-text); }

/* One call waiting to be approved (console-tasks.js actionBlock): who wants
   to run what, the one-line sense of it, and the arguments whole. The
   arguments are a <pre> that scrolls inside its own box - a long URL or a
   pasted body must never make the row, or the page, scroll sideways. */
.task-action {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  font-size: 12px;
  color: var(--text);
}
.task-action-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; overflow-wrap: anywhere; }
.task-action-summary { color: var(--text-dim); overflow-wrap: anywhere; }
.task-action-args {
  margin: 0;
  padding: 6px 8px;
  max-height: 200px;
  overflow: auto;
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-inset);
  white-space: pre;
  max-width: 100%;
  box-sizing: border-box;
}
.task-action-verdict { color: var(--text-dim); }
.task-action-denied .task-action-verdict { color: var(--err-text); }
.task-action-done .task-action-verdict,
.task-action-approved .task-action-verdict { color: var(--ok-text); }

/* What an agent asked for and holds for a while, on its Access tab
   (console-agent.js asked). One line per grant and per request; a decided
   request keeps its verdict on the line. */
.access-asked {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.access-grant {
  font-size: 12.5px;
  color: var(--warn-text);
}

.access-request {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 12.5px;
}
.access-request-open { padding: 8px 10px; border: 1px solid var(--warn-text); border-radius: 6px; background: var(--warn-bg); }
.access-request-when,
.access-request-verdict { color: var(--text-dim); }
.access-request-approved .access-request-verdict { color: var(--ok-text); }
.access-request-denied .access-request-verdict { color: var(--err-text); }

/* The brief on a task that needs somebody (console-tasks.js briefBlock):
   what was done, the ask, and the pull request. Plain text in the ordinary
   colour inside the warning-coloured box, so the reasons read as facts and
   the one line that asks stays the one in colour. */
.task-brief {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  font-size: 12px;
  color: var(--text);
}

.task-brief-label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}

.task-brief-done,
.task-brief-comments {
  margin: 0;
  padding: 0 0 0 16px;
  list-style: disc;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-wrap: anywhere;
}
.task-brief-comments { list-style: none; padding-left: 0; }

.task-brief-ask {
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.task-brief-pull {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 2px;
  padding: 6px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  background: var(--bg-inset);
  min-width: 0;
}

.task-brief-pull-title { font-weight: 600; overflow-wrap: anywhere; }
.task-brief-link { color: var(--text); text-decoration: underline; }
.task-brief-pull-reviews { font-size: 11.5px; color: var(--text-dim); }

/* The facts a merge turns on, as chips in the colour of the answer. */
.task-brief-pull-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 2px 0;
}

/* The description is folded until asked for: the summary is the one line
   "Description", and open, the text is drawn as the markdown it was
   written in, at the card's small size, scrolling inside itself past a
   screen's worth rather than pushing the buttons off the page. */
.task-brief-pull-desc { min-width: 0; }
.task-brief-pull-desc-summary {
  cursor: pointer;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-dim);
  user-select: none;
}
.task-brief-pull-desc-summary:hover { color: var(--text); }
.task-brief-pull-desc[open] .task-brief-pull-desc-summary { margin-bottom: 4px; }

.task-brief-pull-body {
  margin: 0;
  font-size: 11.5px;
  color: var(--text);
  overflow-wrap: anywhere;
  max-height: 28em;
  overflow-y: auto;
  padding: 2px 8px 2px 10px;
  border-left: 2px solid var(--border);
}

.task-brief-md > :first-child { margin-top: 0; }
.task-brief-md > :last-child { margin-bottom: 0; }
.task-brief-md p { margin: 4px 0; }
.task-brief-md h4,
.task-brief-md h5,
.task-brief-md h6 { margin: 8px 0 2px; font-size: 12px; font-weight: 600; color: var(--text); }
.task-brief-md ul,
.task-brief-md ol { margin: 4px 0; padding-left: 18px; }
/* The card is an <li> itself, so without this the browser draws the description's bullets as a nested list's circles. */
.task-brief-md ul { list-style: disc; }
.task-brief-md li { margin: 2px 0; }
.task-brief-md code {
  font-family: var(--font-mono);
  font-size: 10.5px;
  padding: 0 4px;
  border-radius: 3px;
  background: var(--bg);
}
.task-brief-md pre {
  margin: 4px 0;
  padding: 6px 8px;
  border-radius: 4px;
  background: var(--bg);
  font-family: var(--font-mono);
  font-size: 10.5px;
  white-space: pre;
  overflow-x: auto;
}

/* The clock and the bill, one line each - see console-tasks.js budgetLine
   and spentLine. Over budget is the one that changes colour: a task that is
   late is the one somebody is about to ask about. */
.task-budget,
.task-spent,
.task-attempt {
  font-size: 11px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.task-budget-over { color: var(--warn-text); }
.task-failed .task-attempt { color: var(--err-text); }

/* What the agent holding it is doing this second. Same dot as the chat's
   "X is working" line, because it is the same fact seen from the task. */
.task-doing {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--text);
}

/* The steps so far: the agent's own lines quoted, its calls plain. Small,
   because on a task with many they are the bulk of the row, and read for
   the last few. */
.task-steps { margin-top: 3px; }

.task-step-list {
  list-style: none;
  margin: 0;
  padding: 0 0 0 9px;
  border-left: 2px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.task-step {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

.task-step-said { color: var(--text-dim); font-style: italic; }

/* What came to the task from outside - a review, a person's answer - set
   apart from what the agent said and did: it is the step that ended a wait. */
.task-step-heard { color: var(--warn-text); }

.task-step-failed .task-step-text { text-decoration: line-through; }

.task-step-flag {
  margin-left: 6px;
  color: var(--warn-text);
  font-style: normal;
}

.task-steps-btn {
  border: none;
  background: none;
  padding: 1px 0;
  margin-top: 2px;
  font: inherit;
  font-size: 11px;
  color: var(--text-faint);
  cursor: pointer;
  text-decoration: underline;
}

.task-steps-btn:hover { color: var(--text); }

/* The names on the "X → Y" line are links: the two agents most worth
   opening from a task. Underlined only on hover, so the line still reads
   as a sentence. */
.task-agent {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted var(--border-strong);
}

.task-agent:hover { color: var(--text); border-bottom-style: solid; }

/* A task from another repo, on this repo's trail. */
.task-elsewhere {
  font-size: 11px;
  color: var(--text-faint);
  font-style: italic;
}

/* The closing report: what a settled row is read for, so it is set apart
   from the progress lines above it and labelled with which ending it was.
   Done is plain - it is the expected ending; the two others take the
   colour of the mark. */
.task-outcome {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: 4px;
  padding: 6px 9px;
  border-left: 2px solid var(--border-strong);
  background: var(--bg-inset);
  border-radius: 0 5px 5px 0;
}

.task-outcome-label {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.task-outcome-text {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text);
  overflow-wrap: anywhere;
}

.task-failed .task-outcome { border-left-color: var(--err); }
.task-failed .task-outcome-label { color: var(--err-text); }
.task-declined .task-outcome { border-left-color: var(--text-faintest); }

/* What it came to, as chips that go somewhere: the kind, the ticket with
   Linear's word on it, the pull requests, the runs. One line, wrapping. */
.task-links {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 4px;
}

.task-links .chip { text-decoration: none; }
.task-kind { color: var(--text-faint); }

/* What it was done with: one line per kind of thing, name and count. A
   thing that failed is amber - the row is where "the Linear connector
   keeps failing" is first seen. */
.task-used {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 3px 10px;
  margin: 4px 0 0;
}

.task-used-label {
  font-size: 10.5px;
  color: var(--text-faint);
  padding-top: 2px;
}

.task-used-row {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.task-used-count { color: var(--text-faint); }
.chip-warn .task-used-count { color: inherit; }

/* The heading with the view switch beside it: List, which the tab opens
   on, and Trail. */
.queue-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.queue-view {
  display: inline-flex;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  overflow: hidden;
}

.queue-view-btn {
  border: none;
  background: none;
  padding: 3px 10px;
  font: inherit;
  font-size: 11px;
  color: var(--text-dim);
  cursor: pointer;
}

.queue-view-btn + .queue-view-btn { border-left: 1px solid var(--border-strong); }
.queue-view-btn:hover { color: var(--text); }
.queue-view-on { background: var(--bg-inset); color: var(--text); font-weight: 500; }

/* The trail: boxes and the lines between them, drawn at their natural size
   and scrolled sideways past the column's width - never shrunk, since a
   box shrunk to fit a phone has no readable title in it. */
.task-trail-scroll {
  overflow-x: auto;
  margin-top: 8px;
  padding-bottom: 6px;
}

.task-trail {
  display: block;
  max-width: none;
  font-size: 12px;
}

.trail-edge {
  fill: none;
  stroke: var(--border-strong);
  stroke-width: 1.5;
}

.trail-edge-retry { stroke-dasharray: 5 4; }

.trail-node { cursor: pointer; }

.trail-node rect {
  fill: var(--bg-inset);
  stroke: var(--border-strong);
  stroke-width: 1;
}

.trail-node:hover rect,
.trail-node:focus-visible rect { stroke: var(--accent-dim); }
.trail-node:focus-visible { outline: none; }
.trail-node-on rect { stroke: var(--accent); stroke-width: 2; }
.trail-node-elsewhere rect { stroke-dasharray: 4 3; }

.trail-mark { fill: var(--text-faintest); font-size: 11px; }
.trail-node-open .trail-mark { fill: var(--accent); }
.trail-node-accepted .trail-mark { fill: var(--ok); }
.trail-node-blocked .trail-mark { fill: var(--warn-text); }
.trail-node-failed .trail-mark { fill: var(--err); }

.trail-title { fill: var(--text); font-weight: 500; }
.trail-node-done .trail-title,
.trail-node-declined .trail-title { fill: var(--text-dim); }
.trail-owner { fill: var(--text-faint); font-size: 10.5px; }

/* The task opened from a box, under the drawing. */
.task-trail-detail:not(:empty) {
  margin-top: 6px;
  border-top: 1px solid var(--border);
}

/* The repo's queue: the same rows under a heading per state, and the
   heading carries the count so "is anything stuck" is answered before a row
   is read. */
.queue-heading {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 14px 0 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.queue-count {
  padding: 0 6px;
  border-radius: 999px;
  background: var(--bg-inset);
  font-weight: 500;
  color: var(--text);
}

.queue-hint {
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-faint);
}

/* Behind a button, because most of the time nobody is asking - and when they
   are, they are asking about one task rather than all of them. */
.task-cost-btn {
  flex: 0 0 auto;
  border: 1px solid transparent;
  background: none;
  color: var(--text-faint);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
  padding: 1px 7px;
  border-radius: 4px;
}

.task-cost-btn:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.task-cost:not(:empty) {
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg-inset);
}

.cost-rows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  margin: 0;
}

.cost-label {
  color: var(--text-faint);
  font-size: 11px;
}

.cost-value {
  margin: 0;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
}

.cost-number {
  font-family: var(--font-mono);
  font-size: 12px;
}

/* Every number here is either a measurement or an estimate, and the note is
   what says which. It is not decoration - see costs.js. */
.cost-note {
  color: var(--text-faintest);
  font-size: 10.5px;
}

.danger-btn {
  margin-left: auto;
  border: 1px solid transparent;
  background: none;
  color: var(--text-faint);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
  padding: 2px 8px;
  border-radius: 5px;
}

.danger-btn:hover {
  border-color: var(--err);
  color: var(--err);
}

.row-id {
  font-family: var(--font-mono);
  font-size: 12px;
}

.row-note {
  color: var(--text-faint);
  font-size: 11px;
}

.sandbox-new { gap: 6px; }

/* A machine and, folded under it, the form that keeps it. The wrapper carries
   the divider so the form does not get one of its own - two lines between two
   machines reads as two machines missing. */
.sandbox-entry { border-top: 1px solid var(--border); }
.sandbox-entry:first-child { border-top: none; }
.sandbox-entry .row { border-top: none; }

.template-form[hidden] { display: none; }

.template-form {
  padding-bottom: 8px;
  padding-left: 12px;
  border-left: 2px solid var(--border);
}

.template-form label {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Starting an agent on a machine. The checkboxes are the invite dialog's,
   and take the full width for the same reason they do there - a hint beside
   each is what makes "Deployments" a decision rather than a word. */
.resident-form .resident-caps { flex-basis: 100%; }

.resident-form .cap { align-items: flex-start; gap: 8px; }

/* Saved machines sit under the list of running ones, in the same panel: they
   are the same kind of thing, one of them just isn't switched on. */
.template-section {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.text-input,
.steer-select {
  padding: 4px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.text-input { flex: 1; min-width: 140px; }

/* ------------------------------------------------------------- the room */

/* The field under a conversation - the repo's and an agent's, one look.
   A plain textarea and a Send: it was a "Steer it" block with a heading, a
   paragraph and a picker, and talking to an agent should look like talking. */
.chat-input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  resize: vertical;
}

.chat-input:focus {
  outline: none;
  border-color: var(--accent-dim);
}

.chat-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}

.chat-log {
  max-height: 260px;
  overflow-y: auto;
  margin-bottom: 10px;
  padding-right: 4px;
}

.chat-line {
  padding: 6px 0;
  border-top: 1px solid var(--border);
}

.chat-line:first-child { border-top: none; }

.chat-who {
  font-weight: 500;
  font-size: 12px;
}

/* Marked, not hidden - the same rule the room itself follows. Who said this
   is a question worth answering honestly. */
.chat-agent .chat-who { color: var(--accent); }

.chat-about,
.chat-when {
  margin-left: 8px;
  color: var(--text-faint);
  font-size: 10.5px;
}

.chat-text {
  margin: 2px 0 0;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
}

/* Who a line is for. Lit so that a room with three agents in it reads as a
   conversation rather than as a log, and so a tag typed wrong is seen. */
.chat-tag {
  color: var(--accent);
  font-weight: 500;
}

/* The private line is the whole tab, not a panel in a column, so it gets the
   height a conversation wants. */
.chat-log-tall {
  max-height: min(60vh, 520px);
  min-height: 160px;
}

/* An agent being got up for the line just sent - see console-chat.js. */
.chat-status {
  margin: 8px 0 0;
  padding: 6px 10px;
  border: 1px solid var(--warn-border);
  border-radius: 6px;
  background: var(--warn-bg);
  color: var(--warn-text);
  font-size: 12px;
  line-height: 1.5;
}

/*
 * What the agents in the box are doing this moment - "Navigator is
 * thinking…" - see console-chat.js `workingLine`. Not the warning box above
 * it: nothing is wrong, something is happening, and the only colour is the
 * dot, which pulses because an agent in motion is the one thing that does.
 */
.chat-doing {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 8px 0 0;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.5;
}

.chat-doing-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: rail-pulse 1.6s ease-in-out infinite;
  flex: none;
}

@media (prefers-reduced-motion: reduce) {
  .chat-doing-dot { animation: none; }
}

.share-form {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.share-form .text-input { flex: 1; }

/* The one thing on the page that cannot be undone. */
.danger-panel {
  border-color: var(--err-border);
}

.ghost-btn.danger:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* --------------------------------------------------------- what is running */

/* A resident's process log, on the agent page. */
.service-logs {
  margin: 0 0 8px;
  padding: 8px 10px;
  max-height: 220px;
  overflow: auto;
  border-radius: 6px;
  background: var(--bg-deep);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  white-space: pre-wrap;
}

.service-logs[hidden] { display: none; }

/* The two panels nobody opened this page for. Still here, still findable,
   no longer competing with the ones people came for.

   Separated rather than dimmed. Fading them was the first attempt and it
   trades legibility for hierarchy: the text inside is already --text-dim, and
   a second multiplier on top of that pushes it toward failing contrast - for
   everybody, permanently, since a hover rule does nothing on a touch screen.
   A rule and a gap say "these are a different kind of thing" just as well and
   cost nobody anything. */

.detail-settings {
  margin-top: 32px;
  padding-top: 4px;
  border-top: 1px solid var(--border);
}

/* A link inside a hint, which otherwise inherits the hint's flatness and
   reads as ordinary prose. */
.hint-link {
  color: var(--accent);
  text-decoration: none;
}

.hint-link:hover { text-decoration: underline; }

/* --------------------------------------------------- sandboxes, by repo */

.sandbox-group + .sandbox-group {
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.sandbox-group-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 2px;
}

/* The repo is what the row is about; the machines under it are detail. */
.sandbox-group-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
}

/* Turning one off lives at the end of its heading, away from the machines it
   would destroy - a destructive control next to a list reads as being about
   whichever row it is nearest. */
.sandbox-group-head .danger-btn,
.sandbox-group-head .ghost-btn {
  margin-left: auto;
}

.sandbox-group .row {
  padding-left: 10px;
}

/* ------------------------------------------------------------ the repository */

/* A repo that came from GitHub: where, what changed, and the way back.
   Three headings in one panel rather than three panels, because they are one
   story read top to bottom - this repository, these changes, this pull
   request - and a person with the third question has the first two. */
.fact-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 14px;
  margin: 0 0 8px;
  font-size: 13px;
}

.fact-list dt { color: var(--text-dim); }
.fact-list dd { margin: 0; overflow-wrap: anywhere; }

.github-change-list {
  margin: 0;
  padding-left: 18px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.6;
}

.github-change-list .is-deleted { color: var(--text-dim); text-decoration: line-through; }

.github-pr-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 560px;
}

.github-pr-form .text-input { width: 100%; box-sizing: border-box; }
.github-pr-form .panel-actions { display: flex; gap: 8px; }
.github-pr-form textarea.text-input { resize: vertical; }
.github-outcome p { margin: 6px 0 0; font-size: 13px; }
.github-outcome a { overflow-wrap: anywhere; }

/* -------------------------------------------------------- the invite modal */

/* The permission grid: presets on top, then one fieldset per group, two
   abreast where there is room. A fieldset is the honest element - a legend
   with a box that ticks the group, and the group's own boxes under it - and
   it comes with a browser border and padding that are undone here. */
.perm-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  gap: 10px 16px;
}

.perm-presets {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.perm-presets-label {
  color: var(--text-faint);
  font-size: 11px;
  margin-inline-end: 2px;
}

.perm-preset { padding-block: 3px; font-size: 12px; }

.perm-group {
  margin: 0;
  padding: 8px 10px 10px;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
}

/* A legend sits in the fieldset's border by default, with the hint under it
   printed across the line. Floated, it is an ordinary block at the top of
   the box, which is all it was ever meant to be here. */
.perm-legend {
  float: left;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 0;
}

.perm-group-all {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.perm-group-label {
  font-size: 12.5px;
  font-weight: 600;
}

.perm-items {
  clear: both;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 8px;
}

.perm-empty { clear: both; margin: 0; padding-top: 6px; }

/* The marks after a label: "sensitive" is the one to read before ticking. */
.perm-mark {
  margin-inline-start: 6px;
  padding: 0 5px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text-faint);
  font-size: 10px;
  vertical-align: 1px;
  white-space: nowrap;
}

.perm-mark-sensitive {
  border-color: var(--warn-border);
  background: var(--warn-bg);
  color: var(--warn-text);
}

.cap-grid {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cap {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  cursor: pointer;
}

.cap-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.cap-label { font-size: 12.5px; }

.cap-hint {
  color: var(--text-faint);
  font-size: 11px;
}

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

.credential-value {
  flex: 1;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg-inset);
  font-family: var(--font-mono);
  font-size: 11px;
  overflow-x: auto;
  white-space: nowrap;
}

/* ----------------------------------------------------------- a new agent */

/* Three numbered steps across the top of the dialog. The current one is lit,
   the ones behind it are ticked in the accent, the ones ahead are faint -
   so the strip says both where you are and how much is left. */
.wizard-steps {
  display: flex;
  gap: 14px;
  margin: 0 0 4px;
  padding: 0;
  list-style: none;
}

.wizard-step {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-faint);
  font-size: 12px;
}

.wizard-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  font-size: 10.5px;
  font-weight: 600;
}

.wizard-step.current { color: var(--text); }

.wizard-step.current .wizard-step-n {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

.wizard-step.done .wizard-step-n {
  border-color: var(--accent-dim);
  color: var(--accent);
}

/* The where-it-runs choice: three cards, one radio each. The whole card is
   the label so the target is the card, not the 13px circle. */
.choices {
  display: grid;
  gap: 8px;
}

.choice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
  cursor: pointer;
}

.choice input { margin-top: 3px; }

.choice.chosen { border-color: var(--accent); }

.choice.disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.choice-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.choice-title {
  font-size: 13px;
  font-weight: 600;
}

.choice-body {
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.5;
}

.choice-note {
  color: var(--warn-text);
  font-size: 11.5px;
  line-height: 1.45;
}

/* Machines it could live in: one row per sandbox, chosen by radio. */
.pick-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}

.pick {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-top: 1px solid var(--border);
  cursor: pointer;
}

.pick:first-child { border-top: none; }
.pick:hover { background: var(--bg-hover); }
.pick.chosen { background: var(--bg-active); }

.pick-text {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}

.pick-name { font-size: 12.5px; }

/* One repo per block, ticked or not; the permissions open under a
   ticked one. */
.grant-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.grant {
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
}

.grant.chosen { border-color: var(--border-strong); }

.grant-head {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.grant-name {
  font-size: 12.5px;
  font-weight: 600;
}

.grant .perm-grid {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------- it has never connected */

/* Above the tabs, because it is not one part of the page: it is the reason
   every other part is empty. Neutral while it is still reasonable to wait,
   the warning colours once it is not. */
.notice-panel {
  border-color: var(--border-strong);
}

.notice-panel.notice-warn {
  border-color: var(--warn-border);
  background: var(--warn-bg);
}

.notice-warn .panel-heading { color: var(--warn-text); }

/* Something is stopping it. The error colours, because unlike "not yet
   connected" this does not get better by waiting; and the reason shown
   whole, in the words it was given, since that sentence is what the person
   has to act on. */
.notice-panel.notice-err {
  border-color: var(--err-border);
  background: var(--err-bg);
}

.notice-err .panel-heading { color: var(--err-text); }

.trouble-message {
  margin: 0 0 6px;
  padding: 8px 10px;
  border-radius: 6px;
  background: var(--bg-deep);
  color: var(--err-text);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

.trouble-when { margin-top: 0; }

/* Restart and log, each a button with what it said under it. */
.restart-check,
.log-check {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 8px 0;
}

.restart-check .ghost-btn,
.log-check .ghost-btn { align-self: flex-start; }

.notice-steps {
  margin: 0 0 10px;
  padding-left: 20px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.55;
}

.notice-steps li + li { margin-top: 4px; }

.process-check {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 8px 0;
}

.process-check .ghost-btn { align-self: flex-start; }

.process-out {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.process-out:empty { display: none; }

/* A tab with something under it that wants looking at. */
.detail-tab-warn::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--warn-text);
  vertical-align: middle;
}

/* ------------------------------------------------------------- workflows */
/* The Workflows page (console-workflows.js): the line of engines with a
   button to read them now, the run's steps with a step's log under them,
   and Run again on one of this app's own. The tables are list-table; the
   log is the one pre on the page, and it scrolls inside its own box. */
.workflow-engines {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px 16px;
  margin-bottom: 14px;
}
.workflow-engine-list {
  flex: 1 1 320px;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.workflow-engine {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  font-size: 12.5px;
}
.workflow-engine-name { font-weight: 600; }
.workflow-engine-note { color: var(--text-faint); }
.workflow-engine-problem { color: var(--err-text); }
.workflow-sync { flex: 0 0 auto; }

/* Where a pipeline's runs happen, said once at the top of its page: a
   label, the machine in two words, and the sentence under it. It is a
   block rather than another chip because the answer is a sentence - whose
   account, which template, who is billed - and a chip would truncate it. */
.workflow-where {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  margin-bottom: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-inset);
}
.workflow-where-label {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}
.workflow-where-name { font-weight: 600; font-size: 13px; }
.workflow-where-note { flex: 1 0 100%; margin: 2px 0 0; font-size: 12.5px; color: var(--text-dim); }
.workflow-where-ids {
  flex: 1 0 100%;
  margin: 4px 0 0;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

/* Run a DAG of your own: the example spec, shown whole because it is the
   whole of what a caller has to write. It borrows .workflow-log's box, so
   it wraps and scrolls the same way on a phone. */
.workflow-own-spec { max-height: none; }

.workflow-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 12px;
}
.workflow-actions .console-hint { margin: 0; }
.workflow-spec { margin-bottom: 12px; font-size: 12.5px; }
.workflow-spec summary { cursor: pointer; color: var(--text-dim); }

.workflow-run-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
}
.workflow-run-head .panel-heading { margin-bottom: 0; }
.workflow-run-note { font-size: 12px; color: var(--text-faint); }
.workflow-run-message { margin: 6px 0 10px; font-size: 13px; }
/* What became of a failed run, in one box under its head: the reason,
   where it failed, and the run's own output folded. Red-tinted like a
   problem line, because it is one, but a box rather than a line: it has
   three parts and the steps table sits under it. */
.workflow-run-failure {
  margin: 8px 0 12px;
  padding: 10px 12px;
  border: 1px solid var(--err-border);
  border-radius: 8px;
  background: var(--err-bg);
}
.workflow-run-reason {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--err-text);
  overflow-wrap: anywhere;
}
.workflow-run-where {
  margin: 4px 0 0;
  font-size: 12.5px;
  color: var(--text);
}
.workflow-run-where .workflow-run-step {
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  vertical-align: baseline;
}
.workflow-run-output { margin-top: 6px; }
.workflow-run-output summary { cursor: pointer; font-size: 12.5px; color: var(--text-dim); }
.workflow-run-output > pre { margin: 8px 0 0; max-height: 240px; background: var(--bg); }
.workflow-steps-table .list-row.failed > .list-open { box-shadow: inset 3px 0 0 var(--err); }
/* What a run may ask this app for - see workflow-grant.js. Faint: it is a
   fact about the run, not the thing the reader came for. */
.workflow-run-uses {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--text-faint);
  font-family: var(--font-mono);
}
.workflow-run-panel .list-table { margin-top: 10px; }

.workflow-step { margin-top: 12px; }
.workflow-step-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
}
.workflow-step-name { font-weight: 600; }
.workflow-step-note { font-size: 12px; color: var(--text-faint); }
.workflow-step-message { margin: 6px 0 0; font-size: 12.5px; color: var(--err-text); }
/* The log: the tail the server kept, in its own scrolling box, wrapped
   so a long line never widens the page - which is what keeps the pane
   whole on a phone. */
.workflow-log {
  margin: 10px 0;
  padding: 10px 12px;
  max-height: 420px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-inset);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ------------------------------------------------------------- the phone */

/*
 * One column. The pages become a strip across the top and the page is
 * everything under it. The same single breakpoint the rest of the app uses
 * - a second one would be a second layout to keep true.
 */
@media (max-width: 860px) {
  /* On a phone the reason is the row - every cell beside it is gone
     (.list-cell:not(.keep)) - so the row grows down rather than centring
     a tall name against a one-line chip. It already wraps at both widths. */
  .adoption-table .list-row > * { align-items: flex-start; }

  /* A phone has nowhere to float a bubble: anchored to the mark it hangs
     off whichever edge the mark is near, and there is no width to spare
     either side. So down here the answer is not a bubble at all - it flows
     into the page under the word it explains and pushes what follows down,
     which cannot overflow anything. It is only ever there after a tap. */
  .help {
    position: static;
  }

  .help-bubble {
    position: static;
    display: block;
    width: auto;
    max-width: 100%;
    margin-top: 6px;
    transform: none;
    box-shadow: none;
  }

  /* The tiles are two to a row down here, and a sentence in half of one is
     a column of two words a line. So the tile holding the open answer takes
     the whole row for as long as it is open, and the sentence reads across
     the screen like the rest of the page. */
  .metric:has(.help-bubble:not([hidden])) {
    grid-column: 1 / -1;
  }

  /* A harness card's figures are a label and its before → after pair. At
     390px the two columns leave the label three words wide and one word a
     line, so down here the pair sits under its own label - a taller card
     that can be read, rather than a compact one that cannot. */
  .harness-figures {
    grid-template-columns: 1fr;
    gap: 0 0;
  }

  .harness-figures dt { margin-top: 6px; }

  /* And the two windows of the confound strip go one under the other, for
     the same reason: "before: Ada ×4" and "after: Bo ×6" side by side at
     this width is two columns of one word. */
  .harness-beside-row { flex-direction: column; }

  /* A phone has no room for the sentence and the button side by side: the
     demo band stacks, and its button goes full width under it the way every
     other button does down here. Squeezed instead, the button reads as two
     characters and the sentence as one word a line. */
  .demo-band {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 10px 12px;
  }

  .demo-band-btn {
    margin-left: 0;
    width: 100%;
    text-align: center;
  }

  /* Search's box: the field takes a row of its own and the two buttons
     share the one under it, so neither is a sliver beside the field. */
  .search-form {
    flex-wrap: wrap;
  }
  .search-form .search-input {
    flex-basis: 100%;
  }
  .search-form .search-submit,
  .search-form .search-assist {
    flex: 1 1 0;
  }
  /* And the chat's box under the answer, the same way. */
  .search-chat-form {
    flex-wrap: wrap;
  }
  .search-chat-form .search-chat-send {
    flex-basis: 100%;
  }
  /* AI assist and Ask are pressed with a thumb. */
  .explain-btn {
    min-height: 40px;
  }
  /* Its histogram keeps every bar; the bars thin rather than the page widening. */
  .search-histogram-bars {
    height: 72px;
    gap: 1px;
  }
  .search-top-grid {
    grid-template-columns: 1fr;
  }

  .console {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr) auto;
  }

  /* Signed out: one column, in reading order - the pitch, the form, then
     the three things and the way in again. The sky keeps its full bleed;
     only what is on it narrows. */
  .gate-sky { padding: 40px 0 48px; }
  .gate-top { grid-template-columns: minmax(0, 1fr); gap: 28px; padding: 0 16px; }
  .console-gate h1 { font-size: 30px; }
  .gate-intro p { font-size: 16px; }
  .frame { padding: 40px 0; }
  .frame-copy { padding: 0 16px; }
  .frame .frame-copy { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .frame-title { font-size: 23px; }
  .frame-body { padding-top: 0; font-size: 16px; }
  .gate-close { padding: 40px 0 48px; }
  .gate-close-line { font-size: 18px; }
  /* The bar between the public pages: three words, so they fit in one row at
     390px - but the padding narrows with everything else on the sky, and the
     row wraps rather than scrolls if a fourth page is ever added. */
  .gate-nav { padding: 0 16px; margin-bottom: 24px; }
  .gate-sky-plain { padding: 20px 0 40px; }
  .frame-points { font-size: 15px; gap: 10px; }
  /* The chips are the one thing that must not shrink to fit: a cloud's name
     truncated is a cloud a reader cannot find. They wrap, as many rows as
     it takes. */
  .frame-places { margin-top: 16px; }
  .console-gate a.ghost-btn { margin-top: 16px; }
  /* The figures in one column: the four stages stack, each keeping the rule
     over it as its join, and the bridges and chevrons between them go - on a
     column a chevron pointing right points at nothing. The held call and its
     grant stack; a figure's label takes its own line over its bars. The
     trace already scrolls sideways at every width. */
  .frame-figure { margin-top: 28px; }
  .flow { grid-template-columns: minmax(0, 1fr); gap: 22px 0; }
  .flow-stage:not(:last-child)::before,
  .flow-stage:not(:last-child)::after { display: none; }
  /* And the dot that runs the rule: stacked, there is no rule to run. */
  .figure-flow.seen .flow-pulse { display: none; }
  .figure-hold { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  /* How it works' figures in one column too: the boundary's three columns
     stack with the box in the middle, the two Observe panes stack, the
     run's four steps become two rows of two, and Connect's label takes its
     own line over the names. */
  .figure-bound { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .figure-watch { gap: 24px; }
  .dag { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dag-step + .dag-step::before { display: none; }
  .wire-row { grid-template-columns: minmax(0, 1fr); gap: 6px; }
  .measure-row { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  .measure-bar { grid-template-columns: minmax(0, 1fr) 56px; }

  /* Six pages in 390px do not fit in a row, so the strip scrolls sideways
     rather than wrapping, and runs edge to edge so a page cut off at the
     side is visibly cut off - which is what says "there are more". */
  .app-sidebar {
    padding: 6px 8px;
    border-right: none;
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }

  .app-sidebar::-webkit-scrollbar { display: none; }

  /* The workspace joins the strip rather than taking a row of its own: a
     phone has the height to spare less than the width, and the strip
     scrolls anyway. First in it, so it is on screen before any scrolling. */
  .app-sidebar {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .sidebar-workspace {
    flex: 0 0 auto;
    margin-bottom: 0;
  }

  .sidebar-workspace-pick {
    width: auto;
    max-width: 160px;
    min-height: 40px;
  }

  .sidebar-nav {
    flex-direction: row;
    flex: 0 0 auto;
    gap: 4px;
  }

  .sidebar-link {
    flex: 0 0 auto;
    width: auto;
    min-height: 40px;
    padding: 6px 10px;
    white-space: nowrap;
  }

  .console-main {
    padding: 16px 14px 40px;
  }

  /* The footer, at both ends of the app. The gutter narrows with everything
     else, and the mark keeps its row with the two links: three short words
     fit across 390px, and stacking them would spend a thumb's worth of a
     phone screen on a footer. The links themselves grow to a thumb, which is
     the only reason the strip is taller here than the 40px it looks. */
  .site-footer-inner { padding: 12px 16px; gap: 4px 12px; }
  .console-foot .site-footer-inner { padding: 6px 14px; }
  .site-footer-link {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
  }

  /* The About box: a 76px photograph beside a paragraph leaves the paragraph
     four words wide at this width, so the two stack. */
  .founder { flex-direction: column; gap: 12px; }
  /* Stacked, the photograph's column is no longer a column beside anything:
     left-aligned with the paragraph under it, not centred over the width. */
  .founder-who { align-items: flex-start; }

  /* Five tabs in 390px: tighter, and a thumb-sized target. The strip runs
     edge to edge so a tab cut off at the side is visibly cut off, which is
     what says "there are more". */
  .detail-tabs {
    top: -16px;
    gap: 0;
    margin-inline: -14px;
    padding-inline: 14px;
  }

  .detail-tab {
    min-height: 40px;
    padding-inline: 10px;
    font-size: 12.5px;
  }

  /* A row picked with a thumb, and a choice card the same. */
  .pick,
  .grant-head {
    min-height: 40px;
  }

  /* Filters, the tick beside a machine, and the items of the actions menu
     are all things a thumb has to land on. The box grows with the row it sits
     beside rather than with its own size, so the rows keep their shape. */
  .filter-chip,
  .menu-item,
  .list-search,
  .list-actions summary {
    min-height: 40px;
  }

  .filter-chip { padding-inline: 12px; }

  /* A filter dropdown on a phone: the label above the button rather than
     beside it - at 390px a label on the same line leaves the button a
     column two thumbs wide - and the button takes the row it is on, so
     four filters read as four lines and not as a jumble. The list hangs
     off the button as it does anywhere, capped to the viewport so it
     never pushes the page sideways. */
  .filter-row { gap: 10px; }

  .filter-picker {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    width: 100%;
  }

  .filter-picker-shell { width: 100%; }

  .filter-picker-btn {
    width: 100%;
    max-width: none;
    min-height: 40px;
    font-size: 13px;
  }

  .filter-picker-find,
  .filter-picker-option { min-height: 40px; }

  .filter-picker-option { font-size: 13px; }

  .filter-picker-pop { max-width: calc(100vw - 32px); }

  /* The export buttons take the width between them, two to a row at 390px,
     each a thumb tall. `.export-picker` is a `.filter-picker`, so the label
     is already above them rather than beside. */
  .export-btn {
    flex: 1 1 calc(50% - 6px);
    min-height: 40px;
    font-size: 13px;
  }

  .list-pick {
    width: 22px;
    height: 22px;
  }

  /* A phone has room for the name and one fact. The cells go, except the
     one a row marked `keep` - the status - which moves under the name. */
  .list-table,
  .list-table.pickable {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .list-table.pickable { grid-template-columns: 30px minmax(0, 1fr) auto; }

  .list-head { display: none; }
  .list-cell:not(.keep) { display: none; }
  /* One fact, and only one. A row is `display: contents`, so its cells
     fall straight into the two columns above - a second kept cell takes
     the first column of the next line and pushes every row after it round
     by one, which on the ranking put the session's name off the screen
     entirely. Any kept cell after the first goes. */
  .list-row > .list-cell.keep ~ .list-cell.keep { display: none; }
  .list-row > * { min-height: 44px; }
  .list-aside {
    display: block;
    font-size: 12px;
    color: var(--text-dim);
  }

  /* The head does not wrap: it is the dot and the name, and a name that
     wrapped under the dot was a name that had already pushed past the
     card. The name shrinks to what is left of the line instead. */
  .home-card-head { flex-wrap: nowrap; }
  /* One column on a phone: whose and where become a line across the top
     of the card, the name under it, then the facts two to a row. Stretch,
     not flex-start: in a column, flex-start sizes each block to its own
     content, and the block holding a long name grew past the card. */
  .home-card { flex-direction: column; align-items: stretch; gap: 8px; }
  .home-card-side { flex: 0 0 auto; align-self: stretch; flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; padding-right: 0; padding-bottom: 8px; border-right: 0; border-bottom: 1px solid var(--border); }
  .home-card-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .home-fact { min-width: 0; }
  /* The three links one under the other: "Nomad · on nomad-1 · set off by
     a line from Ada in Navigator 3m ago" is two lines at 390px with the
     break landing mid-link, and each link is a thumb's target here. */
  .session-links { flex-direction: column; gap: 4px; }
  .session-links .session-link-sep { display: none; }
  .session-links .session-link { display: inline-block; min-height: 24px; }

  /* The answer box and its Resume take the row's width one under the other
     - at 390px the two side by side leave the box too narrow to read what
     was typed into it. */
  .task-resume { flex-direction: column; }
  .task-resume-note { min-width: 0; width: 100%; box-sizing: border-box; }
  .task-resume-btn { min-height: 44px; }
  /* Four decisions one under the other, each the width of a thumb; the
     arguments box keeps to the column and scrolls inside itself. */
  .task-decide-buttons { flex-direction: column; }
  .task-decide-deny { margin-left: 0; }
  .task-action-args { max-height: 160px; }
  /* The card's buttons one under the other, each the column's width and a
     thumb's height; the merge confirmation wraps the same way. */
  .home-waiting-actions { flex-direction: column; align-items: stretch; }
  .home-waiting-actions .task-resume-btn { width: 100%; box-sizing: border-box; min-height: 44px; }
  .home-waiting-actions .task-btn-quiet { margin-left: 0; }
  .home-waiting-merge,
  .home-waiting-confirm { flex-direction: column; align-items: stretch; }

  /* A platform card is the column's width, and its four facts become four
     lines, label beside value: four across at 390px is three digits each
     and no room for "≥ $1.20". */
  .platform-cards { grid-template-columns: minmax(0, 1fr); }
  .platform-stats { grid-template-columns: auto minmax(0, 1fr); align-items: baseline; }
  .platform-stats dt,
  .platform-stats dd { grid-row: auto; }

  /* The ranking keeps the merged chip - the one fact - under the name, the
     way every table here does; the mark at the edge stays. */
  .perf-table .list-row.effective > .list-open { box-shadow: inset 3px 0 0 var(--ok); }
  /* The comparison's charts stack, and a bar's label column narrows so
     the bar keeps most of a phone's width. */
  .compare-charts { grid-template-columns: minmax(0, 1fr); }
  .chart-row { grid-template-columns: minmax(80px, 108px) minmax(0, 1fr) 52px; gap: 8px; }
  /* The legend wraps to as many lines as it needs rather than scrolling
     sideways: a phone has room for two entries a line and no more. */
  .steer-legend { gap: 4px 10px; }
  .steer-none { font-size: 10.5px; }
  /* Friction: the kind's name goes above its bar rather than beside it -
     at 390px a 132px label column leaves the bar too short to read as a
     length, and the count is the fact anyway. The label row keeps the
     count on its right, so name and number still read as one line. */
  .friction-kind-line {
    grid-template-columns: minmax(0, 1fr) 42px;
    grid-template-areas: "name count" "track track";
    row-gap: 4px;
  }
  .friction-kind-name { grid-area: name; }
  .friction-count { grid-area: count; }
  .friction-track { grid-area: track; }
  .friction-examples { gap: 4px 8px; }
  .compare-tools { flex-direction: column; align-items: flex-start; }
  /* The trend's plot keeps its height; the scale column narrows. */
  .trend-chart { grid-template-columns: 52px minmax(0, 1fr); grid-template-rows: 150px 22px; }
  /* No room for names at the lines' ends on a phone: the legend names them. */
  .trend-chart.is-split .trend-plot,
  .trend-chart.is-split .trend-labels { margin-right: 0; }
  .trend-end { display: none; }
  /* And every other x label between the first and the last goes, so the
     ones left have room. */
  .trend-label:not(:first-child):not(:last-child):nth-child(even) { display: none; }

  /* A tool's agents and tasks stack: title on one line, chip and note
     under it, and the link tall enough to tap. */
  .tools-agent,
  .tools-task { flex-direction: column; align-items: flex-start; gap: 4px; min-height: 44px; justify-content: center; }
  /* The cards stack, and a session line is a thumb's target. */
  /* Workflows: the engines stack - the list's desktop basis is a width,
     and in a column it would be a height - and the log gets less of the
     screen. */
  .workflow-engines { flex-direction: column; }
  .workflow-engine-list { flex-basis: auto; }
  .workflow-log { max-height: 300px; }
  .workflow-own-spec { max-height: none; }
  .workflow-run-output > pre { max-height: 200px; }
  /* A session line on a tool's page is a thumb's target. */
  .tool-session { min-height: 44px; align-content: center; }

  /* A span's line loses its clock; the trace head says when it started
     and the duration is the fact that matters on a narrow screen. */
  .timeline-row { grid-template-columns: minmax(0, 1fr) auto auto; }
  .timeline-time { display: none; }

  /* The transcript likewise: the words take the width, the clock goes,
     and a tool call's duration and chip sit under its name rather than
     squeezing it to a few letters. The box is shorter: on a phone it is
     the screen, and the panels under it have to be reachable. */
  .transcript { max-height: 60vh; }
  .transcript-row { grid-template-columns: minmax(0, 1fr); gap: 3px; }
  .transcript-time { display: none; }
  .transcript-tail:empty { display: none; }
  .steer-stop { min-height: 44px; }

  .history-entry {
    grid-template-columns: 1fr;
    gap: 2px;
  }

  /* Big enough to hit with a thumb.
     Everything on this page was sized for a pointer, which lands within a
     couple of pixels of where it is aimed. A fingertip does not: the usual
     guidance is around 44px, and these were between 17 and 31 - the Back
     button, which is the way out of a thing's page, was 48x20.
     Height rather than padding wherever possible, so the rows they sit in do
     not change shape - a taller target is invisible until it is needed. */
  /* `:not([hidden])` on each, because this sets `display` and comes after
     the `[hidden]` rule at the same specificity - so without it a button a
     page hid is one that reappears below 860px and nowhere else. That is
     the shape of bug this breakpoint keeps producing: right at 1500px,
     wrong on the device nobody has open. */
  .detail-back:not([hidden]),
  .row-edit:not([hidden]),
  .danger-btn:not([hidden]),
  .ghost-btn:not([hidden]),
  .primary-btn:not([hidden]) {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .detail-back {
    padding-inline: 8px 12px;
    margin-inline-start: -8px;
  }

  /* These read as links in a row of text, so they keep their look and gain
     only the room around them. */
  .row-edit,
  .danger-btn {
    padding-inline: 8px;
  }

  /* A link inside a sentence cannot be made 40px tall without pushing the
     sentence apart. Vertical padding on an inline element grows what a finger
     can hit without touching the line box, which is the whole of the problem
     here: it was 14px of target inside a paragraph. */
  .hint-link {
    padding-block: 8px;
  }

  /* One column of groups: two abreast at 390px is two columns of clipped
     labels, and a permission you cannot read is one you tick by guessing. */
  .perm-grid {
    grid-template-columns: 1fr;
  }

  /* A harness row is three lines on a phone: its name and chips, then its
     buttons across the full width - pushed to the end of a 390px row, three
     of them overflow it. And the select-and-Save form stacks. */
  .harness-row .row-edit-actions {
    margin-left: 0;
    flex-basis: 100%;
  }
  .harness-form { flex-direction: column; align-items: stretch; }

  /* The Connect panel on a phone. The summary's heading and its hint stack
     rather than sharing a 390px row, and a snippet's Copy button drops
     under the value instead of squeezing it to nothing - a settings.json
     is the widest thing this console ever asks anybody to copy, and beside
     a button on a phone it would be about forty pixels of it. */
  .credential { flex-wrap: wrap; }
  .credential .ghost-btn { margin-left: auto; }
  .credential-block .credential-value { max-height: 200px; }

  /* An external agent's page on a phone: the repository line and its
     button stack, the dispatch form's two inputs stack, and a tool's
     chips wrap under its name rather than pushing it off the row. */
  .external-config-form,
  .external-dispatch .focus-form { flex-direction: column; align-items: stretch; }
  .external-tool-head { flex-wrap: wrap; }

  /* A task's "used" table on a phone is one column - the label above its
     chips - since "Connectors" beside a wrapping row of chips leaves the
     chips 200px. The view switch keeps its place beside the heading; the
     trail scrolls sideways inside its own box, the column never does. */
  .task-used { grid-template-columns: 1fr; gap: 1px 0; }
  .task-used-row { margin-bottom: 4px; }
  .queue-head { flex-wrap: wrap; }
  .task-trail-scroll { max-width: 100%; }

  /* ---- pull-cost ----
     A phone has no room for a percentage inside a band - a fifth of a
     390px column is 70px and three of those hold nothing - and the legend
     under the bar already names and prices every one of them. */
  .phase-band-share { display: none; }
  .phase-legend { gap: 3px 10px; }
  /* And a pull request's own bar takes the whole line rather than
     squeezing "$4.10 in all" off the side of the screen. */
  .pull-phases .phase-bar { flex-basis: 100%; }
  /* ---- end pull-cost ---- */

  /* ---- merge-blame ----
     The line-by-line panel takes the whole column on a phone: 390px less an
     indent is not enough for code, and the indent said nothing the border
     did not. The share bar goes under the file's name for the same reason,
     and the code box is shorter than a screen so the page can still be
     scrolled past it. It still scrolls sideways inside itself, which is the
     whole arrangement - the column must not. */
  .blame-panel { margin-left: 0; padding-left: 8px; }
  .blame-file-head { align-items: flex-start; }
  .blame-share { flex-basis: 100%; }
  .blame-code { max-height: 320px; }
  .blame-legend { gap: 3px 10px; }
  /* ---- end merge-blame ---- */
}

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

/* The one part of this page where the person does something rather than
   watches. It reads top to bottom like the conversation below it, and is
   deliberately not styled as a chat app - two turns of plain text, and a
   status line that comes and goes while it works. */
.assistant-log {
  display: grid;
  gap: 12px;
  max-height: 420px;
  overflow-y: auto;
}

.assistant-log:empty { display: none; }

.assistant-turn {
  display: grid;
  gap: 3px;
}

.assistant-who {
  color: var(--text-faint);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.assistant-text {
  margin: 0;
  line-height: 1.6;
  /* Model output is plain text and arrives with its own newlines. Without
     this it is one run-on paragraph, which is not what it was written as. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* What it is doing right now. One line, replaced in place - a list of forty
   file reads is an audit trail, and the conversation is where that lives. */
.assistant-doing {
  margin: 0;
  color: var(--text-dim);
  font-size: 12px;
  font-family: var(--font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.assistant-doing::before {
  content: "▸ ";
  color: var(--text-faintest);
}

.assistant-form {
  margin-top: 12px;
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

.assistant-input {
  flex: 1;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-inset);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  line-height: 1.5;
  resize: vertical;
}

.assistant-input:disabled { opacity: 0.6; }

.assistant-form .primary-btn { white-space: nowrap; }

/* ------------------------------------------------------------- connectors */

.connector {
  padding: 10px 0;
  border-top: 1px solid var(--border);
}

.connector:first-of-type { border-top: none; }

.connector-body {
  margin-top: 6px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.connector-body .focus-form { margin-top: 2px; }
.connector-body .text-input { flex: 1; }

/* ------------------------------------------------------------ the builder */

/* Describing an API is a paragraph, not a line. The rest of this page's inputs
   are one-liners, so the textarea inherits `.text-input` and only changes what
   makes it a box. */
.builder-notes {
  min-height: 120px;
  resize: vertical;
  line-height: 1.45;
  font-family: var(--font-mono);
  font-size: 11.5px;
}

/* One endpoint. Boxed, because the list of them is the part somebody has to
   read rather than skim - each row is a decision about what an agent may do. */
.builder-tool {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg-inset);
}

.builder-tool-top {
  display: flex;
  gap: 6px;
  align-items: center;
}

/* The method picker sizes to its content; the path takes what is left. */
.builder-tool-top .steer-select { flex: 0 0 auto; }
.builder-tool-top .text-input { flex: 1; }

/* The reads/writes switch is the point of the row, so it does not sit flush
   against the fields above it as though it were one more of them. */
.builder-tool .cap {
  margin-top: 2px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

/* One executor's numbers (console-executor-stats.js). The range picker is a
   row of small buttons, the chosen one outlined; the day strip is one thin
   bar per day on a baseline, tallest the busiest, a merged day in the
   status colour - the shape of the month, with the figures in the tiles
   above and on hover. The "uses" lists sit in a grid so four short lists
   do not make a tall page. */
.exec-ranges {
  display: flex;
  gap: 6px;
  margin: 0 0 12px;
}
.exec-ranges .ghost-btn.active {
  border-color: var(--text);
  color: var(--text);
}
.exec-days {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 72px;
  padding: 6px 0 0;
  border-bottom: 1px solid var(--border);
}
.exec-day {
  flex: 1 1 0;
  min-width: 3px;
  min-height: 2px;
  background: var(--text-faintest);
  border-radius: 4px 4px 0 0;
}
.exec-day.finished { background: var(--ok); }
.exec-days-ends {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-faint);
}
.exec-uses {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px 20px;
}
.exec-uses-title {
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.exec-uses ul {
  margin: 0;
  padding-left: 18px;
}
.exec-uses-count { color: var(--text-faint); }
