/*
 * 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 front door.
   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 blog carries the sky's
   colour and no photograph - see `.gate-sky-plain`; console.js turns it on
   for the front door and the sign-in page and off for the blog. */
.console-gate.gate-starfield {
  background: var(--sky) url("sky.jpg") center / cover no-repeat;
}

/* The top of a page on the photograph - the bar, the pitch, and on the
   sign-in page the form beside it: no ground of its own, 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 ---
   The 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 six of them is six blog posts.
   Every other band is raised, so the eye finds the joins. */
.frame {
  padding: 72px 0;
  border-top: 1px solid var(--border);
  /* Its own ground, and opaque: the photograph is behind the whole front
     door now, and a band with no ground of its own is black text on a night
     sky. The band that wants the sky (`.frame-own`) says so itself. */
  background: var(--bg);
}

/* Every second band is clear, and the night sky shows through it (owner's
   ask, 2026-09-20). Which bands is decided where they are drawn
   (console-public.js `HowItWorks`), by their place in the list; this only
   says what clear means. It was an `nth-of-type(even)` rule and a second
   shade of white - which counted every `article` on the page, so a band
   added anywhere above would have flipped every ground below it.

   A clear band is dark, so its words take the sky's tokens, the way the
   environment band's always have. Its figure does not: every drawing here
   but the environment's is written in the page's tokens, dark ink for a
   white ground, and on the photograph it would be ink on night. So the
   figure keeps a ground of its own - a white card on the sky - and nothing
   inside it has to know where it is standing. */
.frame.frame-clear {
  background: transparent;
  color: var(--sky-text);
  border-top: none;
}

.frame-clear .frame-body {
  color: var(--sky-text-dim);
}

.frame-clear .frame-eyebrow,
.frame-clear .frame-title {
  color: var(--sky-text);
}

.frame-clear:not(.frame-own) .figure {
  padding: 24px;
  border-radius: 12px;
  background: var(--bg);
  color: var(--text);
}

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

/* On the front door the foot lets the stars through as well, so the
   environment band and the last line are one sky. Only there: the blog's
   scroller has no photograph, and a foot with no ground on it is white
   text on white. */
.gate-starfield .gate-close {
  background: transparent;
}

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

/* The way in, in the site's second colour: Sign in is the press the page
   is asking for, and the accent is every other button - see --attention. */
.gate-close .primary-btn {
  padding: 10px 20px;
  font-size: 14px;
  background: var(--attention);
  color: var(--on-attention);
}

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

/* 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 local edition's strip (public/local.html). There is no public site
   behind it, so there is no Contact and no About us to put here - one line
   saying what this is instead, set on the sky the way the footer's own text
   is, and in the same place along the strip as the mark it replaces. */
.local-foot-note {
  margin: 0;
  padding: 10px 24px 10px 18px;
  color: var(--sky-text-dim);
  font-size: 12px;
}

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

/* --- the box that asks to be let in ---
   The front door's only press while the site is in stealth: two cards for
   which of the two asks this is, then the fields, in one column for the
   reason the contact box is in one. It borrows `.text-input`, `.contact-box`
   and `.choices` rather than restating any of them - what is here is only
   the column and the space between its rows. */
.waitlist-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

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

/* The cards sit a little away from the fields under them: the choice is a
   different question from "who are you", and a uniform gap read as five
   rows of one form. */
.waitlist-form .choices { margin-bottom: 4px; }

/* --- 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 ---
   The two pages and See demo on the left and, pushed to the right, Sign
   in. 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;
  align-items: center;
  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);
}

/* The way in, filled in the site's second colour, so it reads as a press
   and not a third page - and as the press the page is asking for, which
   is what --attention marks everywhere. It is the only sign-in button on
   the page: the form itself is on the sign-in page, behind it (owner's
   ask, 2026-09-14). Signed in the same slot is Console, and reads the same
   way. Pushed to the right edge, on its own: the pages and See demo sit
   together on the left. */
.gate-nav-way {
  margin-left: auto;
  padding: 8px 18px;
  background: var(--attention);
  color: var(--on-attention);
  font-weight: 600;
}

/* On the sign-in page itself the slot is "current", and the two-class
   rule above would paint the glass over the fill: a filled button that
   went dark on the one page it opened would read as disabled. It keeps its
   colour; the page under it is the form, which is indication enough. */
.gate-nav-way:hover,
.gate-nav-way.current {
  background: var(--attention);
  color: var(--on-attention);
}

.gate-nav-way:hover {
  filter: brightness(1.08);
}

/* See demo: filled in the accent, flat, beside the pages - a button in the
   bar rather than a fourth word, so a visitor who is not ready to sign in
   finds the press that needs no account (owner's ask, 2026-09-14). It shone
   for an afternoon - a gradient, a glow and a sheen crossing it - and the
   owner called it weird; a plain fill says "press this" well enough. After
   the link rules above, so its padding, colour and hover win at equal
   specificity - written before them, the link's hover painted its glass
   over the fill. */
.gate-nav-demo {
  padding: 8px 18px;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

.gate-nav-demo:hover {
  background: var(--accent);
  color: var(--on-accent);
  filter: brightness(1.08);
}

/* --- a public page that is not the front door ---
   The front door's sky carries a photograph because it is the first thing
   anyone sees, and the sign-in page keeps it because its top is the front
   door's own. The blog carries the sky's 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] {
  display: none;
}

/* --- the sign-in page ---
   Just the form, centred in what is left of the screen under the bar, and
   the footer at the bottom of the screen rather than wherever the form
   ends: with the pitch gone the page is a short one, and a footer a third
   of the way down with sky under it read as the page having broken off.
   So the slot is at least the scroller's height and a column, the page
   grows to fill it, the sky band takes what the footer leaves, and the
   form is centred in the band. Grown with flex rather than given a
   percentage of its own: the slot's height is auto with a minimum, and a
   percentage against that resolves to nothing - the footer stayed a third
   of the way down with the rule in place. The other public pages are
   taller than the screen and never notice the minimum. */
.gate-page {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

.landing-signin {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
}

.gate-sky-signin {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  padding: 32px 0 48px;
}

.gate-signin {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 24px;
}

/* The form's width is the old panel column's, so the four ways in are the
   size they were beside the pitch, only in the middle now. */
.gate-signin .signin-panel {
  width: 100%;
  max-width: 360px;
}

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

/* A line under a band's points - where the code is, and what the hosted
   thing is instead. Smaller than the body and not faint: it is a sentence
   somebody reads, not a caption. */
/* The one line to run, in the band that hands it over. It is
   console-dom.js's `copyLine`, which is sized for a panel in the console;
   on a landing page it is the thing the whole band is about, so it is
   restated at the size of the figure's own line below it. */
.frame .credential {
  margin-top: 22px;
}

.frame .credential-value {
  padding: 12px 14px;
  border-color: var(--border-strong);
  border-radius: 10px;
  font-size: 14.5px;
}

/* 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 numbered like the rest: it was out of the count for a while, and a
   page that goes 01, blank, 02 reads as a page that lost count. */
/* Two classes, not one. While the grounds were counted by an
   `nth-of-type(even)` rule that rule was two selectors' worth of
   specificity, and whether this band was an even one was an accident of how
   many bands the page had. Written singly, this lost, and the band came out
   near-white text on a near-white ground - legible in the stylesheet and
   invisible on the page. The counting rule is gone (`.frame-clear`, above),
   and the two classes stay: they cost nothing and they cannot lose. */
/* No ground of its own: what is behind it is the photograph, as behind the
   pitch at the top. It was the sky's flat colour while it was the second
   band, and at the foot that read as a dark slab where the stars stopped
   (owner's ask, 2026-09-18). Every second band is clear now
   (`.frame-clear`); this one keeps its own rule because its drawing is
   written in the sky's tokens and wants no white card under it. The
   scroller's own ground is the sky's colour, so this is dark before the
   picture arrives. */
.frame.frame-own {
  background: transparent;
  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 figures ---
   One under each band of How it works (console-figures.js). Each sits in
   the band's own column, under both halves of the copy, so its edges are
   the title's edges. The held call and the comparison came first, on the
   overview that is no longer there; the held call is still written in the
   sky's tokens by default and restated in the page's where it sits on white
   (`.figure-on-page`), because a figure does not know which ground it is
   on. */
/* 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. Get started is the one
   band that does not use this: its drawing is the paragraph column's
   content, beside the copy (see `.frame-start`). */
.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;
}

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

/* Observe: three runs of one task on one time scale. Each row is who ran
   it and a track of segments, one per step, its width the step's minutes
   (`--m`, a flex weight) against the longest run (`--w`, the track's share
   of the lane). The colours say what kind of step: a prompt in the accent,
   a tool call in the page's grey, a failed check in the error tint, a
   passed one in the ok tint, a word from a person in orange - the colour
   of every "somebody, now" on the site - and the step still running in
   the accent, breathing. Every segment's label is text in it, clipped
   when the segment is too short for it, with the whole in its title. */
.runs-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 14px;
}

.runs-task {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-dim);
}

.runs-ruler {
  position: relative;
  height: 18px;
  margin: 4px 0 6px 168px;
  border-bottom: 1px solid var(--border);
}

.runs-tick {
  position: absolute;
  left: var(--at);
  bottom: 3px;
  transform: translateX(-50%);
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}

.runs-tick:first-child {
  transform: none;
}

.runs-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 12px;
}

.runs-row {
  display: grid;
  grid-template-columns: 152px minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}

.runs-who {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-top: 4px;
}

.runs-who-name {
  font-size: 13.5px;
  font-weight: 650;
}

.runs-who-where {
  font-size: 12px;
  color: var(--text-faint);
}

.runs-lane {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.runs-track {
  display: flex;
  gap: 3px;
  width: var(--w);
}

.runs-seg {
  flex: var(--m) 1 0;
  min-width: 0;
  height: 30px;
  padding: 0 8px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-inset);
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 550;
  line-height: 28px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: clip;
  box-sizing: border-box;
}

/* A tool call is the page's grey, said outright so every kind has a rule. */
.runs-tool {
  background: var(--bg-inset);
  color: var(--text-dim);
}

.runs-prompt {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

.runs-fail {
  border-color: var(--err-border);
  background: var(--err-bg);
  color: var(--err-text);
}

.runs-ok {
  border-color: var(--ok);
  background: var(--ok);
  color: var(--on-accent);
}

.runs-you {
  border-color: var(--attention);
  background: var(--attention);
  color: var(--on-attention);
  font-weight: 650;
}

.runs-live {
  border-color: var(--accent);
  border-style: dashed;
  background: var(--bg-raised);
  color: var(--accent);
}

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

/* 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. One
   card, with the grant as its foot: it stands beside the band's paragraph
   (`.frame-beside`), so it has a column's width and no more. */
.figure-hold {
  display: block;
}

.hold-card {
  padding: 16px 18px;
  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);
}

/* The card's foot: which grant the call ran under, and the three times
   on its trail, on one line where they fit. */
.hold-grant {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--sky-line);
}

.hold-grant-line {
  font-size: 13px;
  font-weight: 600;
  color: var(--sky-text);
}

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

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

/* --- A band with its figure beside the copy ---
   Get started and Access: title and paragraph in the first column, the
   figure in the second, the two centred on each other. The paragraph is
   under the title, so it takes none of the title offset a paragraph beside
   a title needs. Get started's copy column is the wider one - its
   paragraph side is a drawing that scales, and the line and its button
   need the room to sit on one row without a scroll. */
.frame-beside .frame-copy {
  align-items: center;
}

.frame-start .frame-copy {
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
}

.frame-beside .frame-body {
  padding-top: 0;
  margin-top: 12px;
}

.frame-beside .frame-title + .frame-body {
  margin-top: 20px;
}

/* The line to run, centred under the two sentences rather than flush with
   them: it is the one thing on the band to act on, and centred it reads as
   the band's object rather than as one more line of its text. The box
   shrinks to the line and its button; the max-width is for the phone,
   where the line wraps. */
.frame-start .credential {
  width: fit-content;
  max-width: 100%;
  margin: 22px auto 0;
}

/* The way to the code: the GitHub mark at the end of its sentence, at the
   size of the band's text and in its colour, where "Source: ... MIT." used
   to be a line of small print. Inline, so it wraps with the words and sits
   after the full stop like one more of them rather than in front of the
   sentence as a bullet. A link with a picture in it, so it needs a hit area
   of its own. */
.frame-github {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-left: 8px;
  padding: 3px;
  border-radius: 8px;
  color: var(--text-dim);
}

.frame-github:hover,
.frame-github:focus-visible {
  color: var(--text);
}

.github-mark {
  width: 20px;
  height: 20px;
  fill: currentColor;
}

/* The first band's figure: a prompt typed to an agent on the left, the
   session it becomes in CoderVibes on the right, an arrow between them. The
   one figure on this page that is a drawing (SVG nodes, console-figures.js
   `startFigure`), so its colours are `fill` and `stroke` rather than
   `background` and `color` - the palette tokens are the same ones. */
.figure-start {
  display: grid;
  gap: 14px;
  justify-items: center;
}

.start-svg {
  width: 100%;
  max-width: 640px;
  height: auto;
}

.start-box {
  fill: var(--bg-inset);
  stroke: var(--border-strong);
  stroke-width: 1.4;
}

.start-rule {
  stroke: var(--border);
  stroke-width: 1;
}

.start-dot {
  fill: var(--border-strong);
}

.start-ask {
  fill: var(--text);
  font-family: var(--font-mono);
  font-size: 8.4px;
}

.start-said {
  fill: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 7.8px;
}

/* The clip the typed line is revealed through. Full width here, so with
   motion off the line is simply there; the gate below starts it at nought. */
.start-type-clip {
  fill: none;
}

.start-brand {
  fill: var(--text-dim);
  font-size: 9px;
  font-weight: 650;
  letter-spacing: 0.04em;
}

/* The arrow: a line and a head, no fill - a filled triangle at this size
   reads as a blob. */
.start-arrow-line,
.start-arrow-head {
  fill: none;
  stroke: var(--text-faint);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The column of pages, with the one you land on lit: the console's own
   arrangement, five rows of it, because five pages is what the local
   edition has. */
.start-page {
  fill: var(--border);
}

.start-page-current {
  fill: var(--accent);
}

/* Activity, and the session that just arrived at the top of it: the row is
   lit the way the console lights the row you are on, and carries the three
   things the page puts on one - what it was for, how many tools it called,
   what it cost. */
.start-where {
  fill: var(--text-faint);
  font-size: 7px;
  letter-spacing: 0.04em;
}

.start-session {
  fill: var(--bg);
  stroke: var(--accent);
  stroke-width: 1.2;
}

.start-session-name {
  fill: var(--text);
  font-size: 8px;
  font-weight: 600;
}

.start-session-meta {
  fill: var(--text-dim);
  font-size: 7px;
}

.start-row {
  fill: var(--border);
}

/* --- the other three figures ---
   One under each of the other bands (console-figures.js, `howFigureFor`).
   Access and Measure draw the held call and comparison above; the three
   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. */

/* Your environment, drawn (console-figures.js `envFigure`): three agents
   on the left, the work as solid lines into the gateway at the top of a
   dashed boundary and out of it to the vendors, and CoderVibes at the
   bottom of the box off that path, with dotted lines into it from every
   agent and the gateway and solid ones from it to the store and secrets.
   On the sky, so the whole of it is the sky's tokens; the sidecar's edge
   is the accent, the one outline on the band, because it is the thing the
   band is selling. The drawing scales with the column - it is a shape -
   and the text sizes are in viewBox units, so a phone shrinks the words
   with the boxes. */
.figure-env {
  color: var(--sky-text);
  /* On a phone the drawing keeps a readable size and scrolls sideways in
     its own box (below) rather than shrinking its words to nothing. */
  overflow-x: auto;
}

.env-svg {
  display: block;
  width: 100%;
  max-width: 880px;
  height: auto;
  margin: 0 auto;
  font-family: var(--font-ui);
}

.env-box {
  fill: var(--sky-glass);
  stroke: var(--sky-line-strong);
  stroke-width: 1;
}

.env-sidecar .env-box {
  fill: var(--sky-glass-hover);
  stroke: var(--accent);
  stroke-width: 1.6;
}

.env-name {
  fill: var(--sky-text);
  font-size: 13.5px;
  font-weight: 650;
}

.env-sidecar .env-name {
  font-size: 15.5px;
}

.env-note {
  fill: var(--sky-text-dim);
  font-size: 11.5px;
}

.env-bound-edge {
  fill: none;
  stroke: var(--sky-line-strong);
  stroke-width: 1;
  stroke-dasharray: 5 5;
}

.env-bound-label {
  fill: var(--sky-text);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.1em;
}

.env-line {
  fill: none;
  stroke: var(--sky-line-strong);
  stroke-width: 1.4;
  stroke-linecap: round;
}

/* What reports in: dotted, and in the accent's light tint - the accent
   itself is too close to the sky to read as a line - so the record's path
   reads apart from the work's at a glance and the legend only confirms it. */
.env-collect {
  fill: none;
  stroke: var(--accent-dim);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-dasharray: 2 5;
}

.env-legend-text {
  fill: var(--sky-text-dim);
  font-size: 10.5px;
}

/* Observe: the three runs, then a pipeline run under them. One column at
   every width: side by side, each pane had half the column and the longest
   run's last steps were 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);
}

/* 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 goal ---
   Speed along the bottom, rigor up the side, and the corner everybody is
   headed for. On white, in the page's tokens; the two groups are the two
   series colours the comparison uses, and each is named on the drawing, so
   which dots are whose is never the colour alone. The corner is an outline
   in the accent, not a fill - the palette's rule. */
.goal-svg {
  display: block;
  width: 100%;
  max-width: 560px;
  height: auto;
  margin: 0 auto;
  font-family: var(--font-ui);
  overflow: visible;
}

.goal-axis,
.goal-axis-head {
  fill: none;
  stroke: var(--text);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.goal-quarter {
  fill: none;
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-dasharray: 3 5;
}

.goal-corner {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
}

.goal-corner-label {
  fill: var(--accent);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.1em;
}

.goal-axis-label {
  fill: var(--text-dim);
  font-size: 13px;
  font-weight: 550;
}

.goal-group-label {
  fill: var(--text);
  font-size: 13px;
  font-weight: 600;
}

.goal-series-1 { color: var(--series-1); }
.goal-series-2 { color: var(--series-2); }

.goal-dot {
  fill: currentColor;
  stroke: var(--bg);
  stroke-width: 1.5;
}

/* Where each one trends, left behind as a ring when the dot moves on. */
.goal-ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-dasharray: 2 3;
}

/* Faint by its colour, not by an opacity: the way is there to be followed,
   not read, and nothing outside the motion gate is ever see-through. */
.goal-way {
  fill: none;
  stroke: color-mix(in srgb, currentColor 35%, transparent);
  stroke-width: 1;
}

.goal-note {
  margin: 10px 0 0;
  text-align: center;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-faint);
}

/* --- Invariants, Route, Improve ---
   The three bands the page was turned to face (2026-09-18). Text in panes,
   in the page's tokens: all three sit on white. Green is an outline or a
   weight here, never a fill behind words - the palette's rule - so what is
   in scope, which invariant was chosen and which fortnight is the better
   one are each said by a word or a number as well. */

/* Invariants: the repository beside the one file that scopes it. */
.figure-scope {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 7fr);
  gap: 16px 24px;
  /* Stretched, not started: the file is taller than the tree, and a short
     pane beside a tall one reads as a list that was cut off. */
  align-items: stretch;
}

.scope-pane {
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-inset);
}

.scope-card {
  border-color: var(--border-strong);
}

.scope-pane-title,
.route-side-title,
.improve-proof-title {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.scope-tree {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: grid;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: 13px;
}

.scope-path {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--text-faintest);
}

.scope-inside {
  border-color: var(--accent);
  color: var(--text);
  font-weight: 600;
}

.scope-path-mark {
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--accent);
}

.scope-head {
  display: flex;
  align-items: baseline;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: 17px;
  font-weight: 650;
}

.scope-version {
  color: var(--accent);
}

.scope-file {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text-faint);
}

.scope-summary {
  margin: 10px 0 0;
  font-size: 15px;
  color: var(--text-dim);
}

.scope-lists {
  margin: 14px 0 0;
}

.scope-list {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: 12px;
  align-items: baseline;
  padding: 8px 0;
  border-top: 1px solid var(--border);
}

.scope-list dt {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
}

.scope-list dd {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.scope-item,
.route-key,
.route-row-key {
  padding: 2px 7px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-raised);
  font-family: var(--font-mono);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}

.scope-mode {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
}

.scope-mode-means {
  font-size: 13px;
  color: var(--text-faint);
}

.scope-rule {
  margin: 0;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-size: 14px;
}

.scope-rule-label {
  flex: 0 0 72px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
}

.scope-note,
.improve-team {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-faint);
}

.scope-note {
  grid-column: 1 / -1;
}

/* Route: the ask across the top, what lands under it on the left, and what
   the router weighed on the right. */
.figure-route {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: 12px 24px;
  align-items: start;
}

.route-ask {
  grid-column: 1 / -1;
  font-family: var(--font-mono);
  font-size: 14.5px;
}

.route-block,
.route-side > div,
.improve-card,
.improve-proof {
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-inset);
}

.route-block {
  border-color: var(--border-strong);
}

.route-head,
.improve-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.route-head-title {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 650;
}

.route-line {
  margin: 12px 0 0;
  font-size: 15px;
  line-height: 1.9;
}

/* A name in a sentence goes to the next line whole: broken across two, the
   chip's border is drawn open at both ends and reads as two names. */
.route-key {
  display: inline-block;
  line-height: 1.4;
}

.route-key-chosen,
.route-row-chosen .route-row-key {
  border-color: var(--accent);
  font-weight: 650;
}

.route-why,
.route-out {
  margin: 10px 0 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

.route-out {
  padding-top: 10px;
  border-top: 1px solid var(--border);
  color: var(--text-faint);
}

.route-side {
  display: grid;
  gap: 12px;
}

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

.route-row {
  display: grid;
  grid-template-columns: minmax(0, 11fr) minmax(0, 6fr) 40px;
  align-items: center;
  gap: 10px;
}

.route-row-key {
  justify-self: start;
  max-width: 100%;
}

.route-bar,
.improve-track {
  display: block;
  height: 10px;
  border-radius: 4px;
  background: var(--bar-rest);
  overflow: hidden;
}

.route-fill,
.improve-fill {
  display: block;
  width: var(--w);
  height: 100%;
  background: var(--series-1);
}

.route-share,
.improve-bar-value {
  font-size: 13.5px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text-dim);
}

.route-row-chosen .route-share {
  font-weight: 650;
  color: var(--text);
}

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

.route-patterns li {
  padding: 2px 7px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-raised);
  font-family: var(--font-mono);
  font-size: 12.5px;
}

/* Improve: the recommendation on the left, the fortnights on the right. */
.figure-improve {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: 12px 24px;
  align-items: start;
}

.improve-card {
  border-color: var(--border-strong);
}

.improve-tag {
  padding: 3px 10px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 650;
  color: var(--accent);
}

.improve-said {
  margin: 12px 0 0;
  display: grid;
  gap: 10px;
}

.improve-row dt {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.improve-row dd {
  margin: 2px 0 0;
  font-size: 14.5px;
  line-height: 1.5;
}

.improve-adopted {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 13.5px;
  color: var(--text-dim);
}

.improve-adopted-mark {
  font-weight: 650;
  color: var(--ok-text);
}

.improve-adopted-line {
  font-family: var(--font-mono);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}

.improve-bars {
  display: grid;
  gap: 10px;
  margin: 14px 0;
}

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

.improve-bar-label {
  font-size: 13.5px;
  color: var(--text-dim);
}

.improve-bar-before .improve-fill {
  background: var(--series-2);
}

.improve-bar-after .improve-bar-value {
  font-weight: 650;
  color: var(--text);
}

/* The held call on a white band (Access): the same figure the sky had,
   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 {
  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,
.figure-on-page .hold-grant {
  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 names arrive in order, the session's steps 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 here loops, because a
   thing that keeps moving keeps taking the eye back - the one exception on
   the page is the step still running, an agent at work.

   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`. */
@media (prefers-reduced-motion: no-preference) {
  .figure-hold .hold-card,
  .figure-hold .hold-grant,
  .figure-hold .hold-trail li,
  .figure-hold .hold-outcome {
    opacity: 0;
  }

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

  /* The goal: everybody starts near the middle, drifts to where they trend
     - the careful up and left, the fast down and right - waits there long
     enough to be read, and then goes to the top right. Once. The rings and
     the ways between are what is left behind, so they arrive as the dots
     set off for home. */
  .figure-goal .goal-dot,
  .figure-goal .goal-ring {
    opacity: 0;
  }

  .figure-goal .goal-way {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
  }

  .figure-goal.seen .goal-dot {
    animation: goal-trend 5600ms ease-in-out both;
    animation-delay: calc(var(--i) * 70ms);
  }

  .figure-goal.seen .goal-ring {
    animation: goal-left 600ms ease-out 3300ms both;
  }

  .figure-goal.seen .goal-way {
    animation: goal-draw 1900ms ease-in-out both;
    animation-delay: calc(3400ms + var(--i) * 70ms);
  }

  @keyframes goal-trend {
    0% { transform: translate(var(--sx), var(--sy)); opacity: 0; }
    8% { opacity: 1; }
    40% { transform: translate(var(--dx), var(--dy)); }
    60% { transform: translate(var(--dx), var(--dy)); }
    100% { transform: none; opacity: 1; }
  }

  @keyframes goal-left {
    from { opacity: 0; }
    to { opacity: 1; }
  }

  @keyframes goal-draw {
    to { stroke-dashoffset: 0; }
  }

  /* Invariants: the repository's folders arrive, then the file's lists -
     the order the sentence goes, a big tree and then what is kept of it.
     Route and Improve: the bars grow to their values, as the comparison's
     do. */
  .figure-scope .scope-path,
  .figure-scope .scope-list {
    opacity: 0;
  }

  .figure-scope.seen .scope-path {
    animation: figure-in 320ms ease-out both;
    animation-delay: calc(var(--i) * 60ms);
  }

  .figure-scope.seen .scope-list {
    animation: figure-in 420ms ease-out both;
    animation-delay: calc(800ms + var(--i) * 140ms);
  }

  .figure-route .route-fill,
  .figure-improve .improve-fill {
    width: 0;
    transition: width 900ms cubic-bezier(0.2, 0.7, 0.2, 1);
    transition-delay: calc(300ms + var(--i) * 160ms);
  }

  .figure-route.seen .route-fill,
  .figure-improve.seen .improve-fill {
    width: var(--w);
  }

  /* How it works. 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. */
  /* Get started: the ask is typed, the agent answers it a line at a time,
     then the arrow, then the session arriving on the page. The typing is a
     clip that widens, because an SVG `<text>` has no width to animate; it
     is only ever narrowed in here, so with motion off the line is whole
     from the start and nothing waits on a cue that will not come. */
  .figure-start .start-type-clip {
    width: 0;
  }

  .figure-start.seen .start-type-clip {
    animation: start-type 900ms steps(29) both;
  }

  @keyframes start-type {
    to { width: 240px; }
  }

  .figure-start .start-said,
  .figure-start .start-arrow,
  .figure-start .start-console {
    opacity: 0;
  }

  /* `--i` is the line's, set where it is drawn (console-figures.js), so the
     answer arrives after the question has finished being asked. */
  .figure-start.seen .start-said {
    animation: figure-in 320ms ease-out both;
    animation-delay: calc(900ms + var(--i) * 240ms);
  }

  .figure-start.seen .start-arrow {
    animation: figure-in 380ms ease-out 1700ms both;
  }

  .figure-start.seen .start-console {
    animation: figure-in 520ms ease-out 1900ms both;
  }

  .figure-seek .seek-query {
    display: inline-block;
    max-width: 0;
    overflow: hidden;
    white-space: nowrap;
    vertical-align: bottom;
  }

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

  .figure-env .env-agent,
  .figure-env .env-inner,
  .figure-env .env-vendor,
  .figure-env .env-collect,
  .figure-watch .runs-said,
  .figure-watch .dag-reason,
  .figure-seek .seek-hit {
    opacity: 0;
  }

  /* The environment draws itself in the order the sentence goes: the
     agents arrive, the work's lines run into the gateway and out to the
     vendors, then the records fade in - dotted, down into the sidecar -
     and last its lines to the store and the secrets. `pathLength` is 1 on
     every solid line, so one dash of one unit drawn from offset one to
     nought is the line appearing end to end; the dotted lines keep their
     dashes and fade instead. */
  .figure-env .env-line {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
  }

  .figure-env.seen .env-agent {
    animation: figure-in 380ms ease-out both;
    animation-delay: calc(var(--i) * 160ms);
  }

  .figure-env.seen .env-line {
    animation: env-draw 600ms ease-out both;
    animation-delay: calc(500ms + var(--i) * 220ms);
  }

  .figure-env.seen .env-vendor {
    animation: figure-in 420ms ease-out 1300ms both;
  }

  .figure-env.seen .env-collect {
    animation: figure-in 420ms ease-out both;
    animation-delay: calc(1600ms + var(--i) * 180ms);
  }

  .figure-env.seen .env-inner {
    animation: figure-in 380ms ease-out both;
    animation-delay: calc(2300ms + var(--i) * 220ms);
  }

  /* The runs play left to right on the shared scale, one row after the
     next, each row's line arriving as its track finishes; the step still
     running breathes, the one loop on the page. */
  .figure-watch .runs-track {
    clip-path: inset(0 100% 0 0);
  }

  .figure-watch.seen .runs-track {
    animation: runs-play 1400ms ease-out both;
    animation-delay: calc(var(--i) * 500ms);
  }

  .figure-watch.seen .runs-said {
    animation: figure-in 320ms ease-out both;
    animation-delay: calc(1300ms + var(--i) * 500ms);
  }

  .figure-watch.seen .runs-live {
    animation: runs-going 1600ms ease-in-out 2800ms 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 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 runs-going {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@keyframes runs-play {
  to { clip-path: inset(0 0 0 0); }
}

@keyframes env-draw {
  to { stroke-dashoffset: 0; }
}

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

/* The form's own Sign in, in the second colour too: the same press the
   bar's button promised, on the page it opened. */
.gate-sky .signin-form .primary-btn {
  padding: 11px 14px;
  border-radius: 9px;
  font-size: 13.5px;
  background: var(--attention);
  color: var(--on-attention);
}

.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 front door 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));
}

/* ------------------------------------------- the git hosts (local edition)
 *
 * The Connectors page of `npm run local` (public/page-git-hosts.js): three
 * rows, one per git host, and the last cell of each is a *form* with a token
 * box in it rather than a fact. No other list here has that, and three
 * things follow from it that no other list needs.
 *
 * The column: a name, a token box and a Connect do not fit in the 1fr a
 * cell gets, and Bitbucket wants a username box as well. So this table
 * splits itself down the middle instead. It is written at the same
 * specificity as `.list-table` above and sits between that rule and the
 * 860px block below, which is what lets the phone rule still take the
 * table back to one fact per row.
 *
 * The wrapping: at 390px the boxes do not fit on one line either, and
 * `.focus-form` is the shape of every other form in this console - making
 * them all wrap would move each of them for the sake of this one. So it
 * wraps here and nowhere else.
 *
 * The cell is marked `keep`, so the phone rule does not drop it: a page
 * whose only control is hidden below the breakpoint reads as broken rather
 * than as narrow, which is exactly how three pages shipped once before.
 */
.git-hosts-table {
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
}

.git-host-form,
.git-host-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

/* The refusal: what the host said about the token, under the form, in the
   same cell (page-git-hosts.js). A cell is one line with its overflow
   hidden (`.list-cell` below) because a cell holds a fact - and this is the
   one cell in this console that holds a sentence. So "GitHub did not accept
   that token: Bad credentials" ran off the side of the table and read as
   "GitHub did not accept tha", which is the half that says nothing about
   what to do.

   The cell stacks and the words wrap. `align-items: stretch` so the
   sentence gets the cell's width rather than the form's own flex-end
   alignment, and it is the cell that changes rather than `.console-problem`
   everywhere: the same paragraph in every other panel on every other page
   is in a panel, where it already wraps. */
.git-hosts-table .list-cell.keep {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  overflow: visible;
  white-space: normal;
}

.git-hosts-table .console-problem {
  margin: 0;
  overflow-wrap: anywhere;
}

/* The name, and the chip that says whether it is connected, on one line -
   the chip is in the name rather than in a cell of its own, because a phone
   keeps one cell and that one is the form. */
.list-name-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}


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

/* 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 a task's line
   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 .doing-dot { animation: none; background: var(--text-faint); }
.home-doing.moving .doing-dot { animation: rail-pulse 1.6s ease-in-out infinite; background: var(--accent); }
.home-doing .doing-text { min-width: 0; overflow-wrap: anywhere; }

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

/* ---------------------------------------------------- evaluations

   The Evaluations page: the eval runs as cards (their own block below),
   the worst sessions as a list, the rulebook behind a press. The eval
   form's questions and buttons are the `.eval-adopt-*` ones, named for the
   adopt form they were first written for. The line on Performance that
   points here is `.perf-evaluate`. */
.perf-evaluate {
  margin: 4px 0 14px;
  font-size: 12.5px;
  color: var(--text-dim);
}

.perf-evaluate-link { font-weight: 600; }

.eval-cards { display: grid; gap: 12px; }

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

.eval-card-title {
  font-weight: 600;
  flex: 1 1 160px;
  overflow-wrap: anywhere;
}

.eval-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

/* "Start evaluation", in the Evaluations page's top corner, and what it
   drops down to: invariants, or memories (console-eval-start.js). The
   corner is the head's own (.detail-head.has-corner); the menu hangs from
   the button's right edge so it opens into the page and not off it. */
.detail-head.has-corner .eval-start { grid-area: corner; }
.eval-start { position: relative; }
.eval-start-btn { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
.eval-start-caret {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
}
.eval-start-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  display: grid;
  width: min(360px, calc(100vw - 32px));
  padding: 4px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg);
  box-shadow: 0 8px 24px var(--shadow-soft);
}
.eval-start-item {
  display: grid;
  gap: 2px;
  padding: 8px 10px;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.eval-start-item:hover,
.eval-start-item:focus-visible { background: var(--bg-hover); outline: none; }
.eval-start-item[aria-disabled="true"] { cursor: default; color: var(--text-dim); }
.eval-start-item[aria-disabled="true"]:hover { background: none; }
.eval-start-item-name { font-weight: 600; font-size: 13.5px; }
.eval-start-item-note { font-size: 12px; color: var(--text-dim); line-height: 1.4; }

.eval-adopt-question {
  display: block;
  width: 100%;
  margin-bottom: 4px;
  padding: 0;
  font-size: 12px;
  font-weight: 600;
}

.eval-adopt-choice {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  cursor: pointer;
}

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

.eval-adopt-buttons { display: flex; flex-wrap: wrap; gap: 8px; }

/* A finding chip: the severity is in the hue - three is money spent on
   nothing, two a person coming back, one friction got past, nought a gap
   in the record. */
.eval-findings { display: flex; flex-wrap: wrap; gap: 4px; }
.eval-finding { font-size: 10.5px; }
.eval-severity-3 { background: var(--err-bg); color: var(--err-text); border-color: var(--err-border); }
.eval-severity-2 { background: var(--warn-bg); color: var(--warn-text); border-color: var(--warn-border); }
.eval-severity-0 { color: var(--text-faintest); }

/* Eval runs (console-eval-runs.js): the start form, then one card an eval,
   each mostly its pull requests. The form is the adopt form's shape - a
   question in words over its answers - with the invariants as a column of
   checkboxes, each with its summary under the name, since a name alone
   does not say which setup it is. */
.evalrun-form {
  display: grid;
  gap: 12px;
  margin-top: 4px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
}

.evalrun-question { margin: 0; padding: 0; border: 0; min-width: 0; }
.evalrun-question .eval-adopt-choice { margin-right: 16px; }

.evalrun-choices {
  display: grid;
  gap: 6px;
  max-height: 320px;
  overflow-y: auto;
}

.evalrun-choice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 12.5px;
  cursor: pointer;
}

.evalrun-choice input {
  width: auto;
  margin: 2px 0 0;
  padding: 0;
  border: none;
  background: none;
  accent-color: var(--accent);
}

.evalrun-choice-words { display: grid; min-width: 0; }
.evalrun-choice-name { font-weight: 600; overflow-wrap: anywhere; }
.evalrun-choice-note { color: var(--text-dim); font-size: 11.5px; }

.evalrun-list { margin-top: 12px; }

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

.evalrun-when { font-size: 12px; color: var(--text-dim); }

.evalrun-invariants,
.evalrun-changes,
.evalrun-pull-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  margin: 8px 0 0;
  font-size: 12px;
}

.evalrun-job,
.evalrun-pull-where { color: var(--text-dim); }
.evalrun-link { overflow-wrap: anywhere; }
.evalrun-summary { margin: 10px 0 0; font-size: 12.5px; }
.evalrun-pulls { display: grid; gap: 6px; margin-top: 4px; }
.evalrun-pull { padding-top: 6px; border-top: 1px solid var(--border); }
.evalrun-pull .evalrun-changes { margin-top: 4px; }
.evalrun-change { font-size: 10.5px; overflow-wrap: anywhere; }
.evalrun-change-invariant { background: var(--ok-bg); color: var(--ok-text); border-color: var(--ok-border); }

/* A sweep the eval runner reported (console-eval-runs.js `runnerCard`).
   Its outcomes are a table of configs rather than pull requests, and the
   rate cell is two lines on purpose: the interval sits under the rate at
   every width, because a rate shown without it is the one number on this
   page a reader can act wrongly on. */
.evalrun-runner .list-table { margin-top: 8px; }
.evalrun-rate { display: grid; gap: 1px; }
.evalrun-rate-figure { font-variant-numeric: tabular-nums; }
.evalrun-rate-interval { color: var(--text-dim); font-size: 10.5px; font-variant-numeric: tabular-nums; }
.evalrun-verdict { margin: 8px 0 0; font-size: 12.5px; }
.evalrun-warnings { margin-top: 10px; }
.evalrun-warning { margin: 2px 0 0; font-size: 12px; color: var(--text-dim); }
.evalrun-runner .row-id { font-size: 11px; }

/* --- an eval CoderVibes started itself ---
   What the installation is about to spend, before the button is pressed,
   and what the sandbox it started is doing afterwards. Both are boxed off
   from the questions around them: they are statements, not answers, and a
   person skimming a form should be able to tell at a glance which is which. */
.evalrun-spend,
.evalrun-launch {
  display: grid;
  gap: 4px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-raised);
}

.evalrun-spend-line,
.evalrun-launch-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  margin: 0;
  font-size: 12.5px;
}

.evalrun-spend p,
.evalrun-launch p { margin: 0; }
.evalrun-launch { margin-top: 10px; }

.evalrun-trials {
  width: 5em;
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font: inherit;
  font-variant-numeric: tabular-nums;
}

/* Orange, not green. Green is the press the page is asking for; stopping
   something a person started is the other kind, and --attention is what
   that is everywhere here (see the gate's Sign in). */
.evalrun-stop {
  background: var(--attention);
  border-color: var(--attention);
  color: var(--on-attention);
}

/* The cannot-start block is now several sentences rather than one - each
   missing piece named with what to set - so they are a list to read down
   rather than a paragraph. */
.evalrun-cannot { display: grid; gap: 4px; margin: 4px 0 0; font-size: 12.5px; }
.evalrun-cannot p { margin: 0; }

/* The worst-sessions table: the findings column holds a row of chips, so
   it is the widest after the name and its cell wraps rather than being cut
   at the edge with an ellipsis, which is what a list cell does by default. */
.eval-worst .list-table { grid-template-columns: minmax(0, 2fr) minmax(0, 2.6fr) repeat(4, minmax(0, 0.7fr)); }
.eval-worst .list-cell { white-space: normal; overflow: visible; }

.eval-rulebook-head { cursor: pointer; font-size: 12.5px; }

.eval-rule-list {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 6px 12px;
  margin: 10px 0 0;
  font-size: 12px;
}

.eval-rule-name {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

.eval-rule-means { margin: 0; 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;
}

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

/* A kind of work the range never used: on the table so the list of kinds
   is whole, and quiet so the eye lands on the ones with figures. */
.perf-kind-unused .perf-row-title,
.perf-kind-unused .list-cell { color: var(--text-faintest); }

/* --- invariants --- */

/* The invariant a session ran under, as a chip: monospace, because it is
   a path in the repository and a version, and reads as one. */
.invariant-chip,
.invariant-pattern {
  font-family: var(--font-mono);
  font-size: 12px;
}

/* In a listing's Where cell the chip may be the widest thing in the row -
   a path, a version and a mode - so it wraps there rather than being cut
   at the cell's edge, which read as a chip that ends in a dot. */
.activity-where .invariant-chip {
  white-space: normal;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* An invariant's name on the Performance table is a path: keep it on one
   line where there is room, and let it break at a slash where there is not. */
.perf-invariants-table .perf-row-title {
  font-family: var(--font-mono);
  font-size: 13px;
  overflow-wrap: anywhere;
}

/* The Invariants page: one box, then the cards. The box is the whole of
   the navigation over thousands of files, so it is wide and first. */
.invariants-search { margin: 4px 0 14px; }
.invariants-search-input { width: 100%; max-width: 640px; }

/* The cards, in a grid that fills the pane: three across on a wide desktop,
   two on a narrower one, never one column wider than a sentence reads. */
.invariant-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 12px;
}

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

.invariant-card-retired { opacity: 0.7; }

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

/* The name is a path and a link: monospace, the accent, no underline
   until hovered - the same reading as a row's name in a list. */
.invariant-card-name {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.invariant-card-name:hover { text-decoration: underline; }

.invariant-card-summary { margin: 0; }
.invariant-card-when {
  margin: 0;
  font-size: 13px;
  color: var(--text-dim);
}

/* The figures, in a row that wraps: dots between them, faint for the
   line that says nothing ran. */
.invariant-card-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin-top: 4px;
  font-size: 13px;
}
.invariant-stat-faint { color: var(--text-dim); }
.invariants-note { margin-top: 12px; }
.invariants-refused { color: var(--warn-text); }

/* One invariant's scope: five lists side by side where there is room. */
.invariant-scope {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.invariant-scope-head {
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 4px;
}
.invariant-scope-none { color: var(--text-faintest); font-size: 13px; }
.invariant-scope-entries {
  margin: 0;
  padding-left: 18px;
  font-size: 13px;
}
.invariant-scope-entries li { overflow-wrap: anywhere; }
.invariant-notes {
  margin: 0;
  white-space: pre-wrap;
  font-family: var(--font-mono);
  font-size: 13px;
}

/* The patterns box on an executor's page: one pattern a line. */
.invariants-input {
  width: 100%;
  min-height: 5em;
  font-family: var(--font-mono);
  resize: vertical;
}

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

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

/* And it is in colour, alone on the page. The panel is the one thing
   somebody who has just signed in has to do, and in the faint grey every
   other heading and label wears it read as one more panel - a person
   scrolled past the line they had come for. So its heading and the step's
   title are the accent, at reading size rather than the small capitals,
   the line itself has an accent outline, and Copy - the press that is
   this person's move, the way Answer or Approve is on a waiting task - is
   solid orange (--attention), the one colour nothing else on the page
   uses. Green outlines and a filled button are within the white-ground
   palette; the orange is its rule for the press that is somebody's move. */
.setup-panel { border-color: var(--accent-dim); }
.setup-panel .panel-heading {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--accent);
}
.setup-panel .connect-step .modal-label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--accent);
}
.setup-panel .credential-value {
  border: 1.5px solid var(--accent);
  font-size: 12px;
}
.setup-panel .credential .ghost-btn {
  border-color: var(--attention);
  background: var(--attention);
  color: var(--on-attention);
  font-weight: 600;
  padding: 6px 16px;
}
.setup-panel .credential .ghost-btn:hover { filter: brightness(1.08); color: var(--on-attention); background: var(--attention); }

/* ----------------------------------------------------- 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 card'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 dot beside what an agent is doing this moment - "Navigator is
 * thinking…" - on the session page (`.home-doing`) and on a task
 * (`.task-doing`), which set the type around 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.
 */
.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) {
  .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;
}

.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 of this app's own runner could ask it for, on the runs it
   recorded. 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; }

  /* The eval form's one-line answers - harness, model, task set - sit
     beside each other at desktop width on a 16px right margin each. There
     is no width for that here, so they become a column: a radio whose label
     wrapped onto the next line under the next radio's button is unreadable
     and untappable, which is worse than a taller form. */
  .evalrun-question .eval-adopt-choice { display: flex; margin-right: 0; }

  /* Both statements stack rather than flowing, for the same reason: the
     credential's name and the caps are two facts, not one sentence, and
     half of one wrapping under the other reads as neither. */
  .evalrun-spend-line,
  .evalrun-launch-line { display: grid; gap: 2px; }

  /* A number field that fills the column is a thumb-sized target rather
     than a five-character box somebody has to aim at. */
  .evalrun-trials { width: 100%; }

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

  /* `display: block` above is an author rule, and any author rule beats the
     browser's own `[hidden] { display: none }` - so without this every
     answer on the page is open on a phone from the first paint, under every
     "?", and the tap that should open one closes nothing. Up top the bubble
     sets no display, which is why a desktop never showed it. */
  .help-bubble[hidden] {
    display: 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; }

  /* The rulebook's two columns are one under the other for the same
     reason the harness figures are; and the adopt form's field takes the
     row, since a select and a text box side by side at 390px is a text
     box two words wide. */
  .eval-rule-list { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
  /* The worst-sessions table keeps its findings, and they are a row of
     chips that wraps: beside the name in an `auto` column they took the
     width and left the name twenty pixels, so - as the git hosts do - the
     name takes the line and the findings take the next one. */
  .eval-worst .list-table { grid-template-columns: minmax(0, 1fr); }
  .eval-worst .list-cell.keep { white-space: normal; overflow: visible; }
  /* And no rule between the name and its findings, or the chips read as
     the head of the row under them. */
  .eval-worst .list-row > .list-cell.keep { border-top: 0; padding-top: 0; min-height: 0; }
  .eval-rule-means { margin-bottom: 8px; }

  /* 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: two pages and two presses fit in one
     row at 390px - the padding narrows with everything else on the sky, and
     the row wraps rather than scrolls if a 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 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; }
  /* How it works' figures in one column too: the runs' who-column goes
     over its track, the environment drawing scales with the page, and the
     run's four steps become two rows of two. */
  /* Get started. The line wraps rather than scrolling sideways in its box:
     in the console a credential is one long token and scrolling is the only
     honest thing to do with it, but this is the one line the front door is
     asking somebody to run, and a line whose end is off the edge of a phone
     is a line they cannot read before running. The drawing scales with the
     page - it is a shape, and the line it shows is the copyable one above
     it at full size. On one column the drawing follows the line, and the
     grid's 12px gap is a paragraph's gap, not a figure's. */
  .frame-start .credential-value { white-space: pre-wrap; overflow-wrap: break-word; font-size: 13.5px; }
  .frame-beside .frame-side { margin-top: 20px; }
  /* Your environment on one column: the paragraph and the places before
     the four lines, which sit under the title on a desktop and would
     otherwise come first here - the two halves' wrappers dissolve so the
     grid can order their children. The drawing keeps 720 of its 880 units
     and scrolls sideways, a diagram being the one thing the page lets
     scroll that way. */
  .frame-own .frame-head, .frame-own .frame-side { display: contents; }
  .frame-own .frame-points { order: 1; margin-top: 8px; }
  .env-svg { min-width: 720px; }
  .figure-watch { gap: 24px; }
  .runs-ruler { margin-left: 0; }
  .runs-row { grid-template-columns: minmax(0, 1fr); gap: 4px; }
  .runs-who { flex-direction: row; align-items: baseline; gap: 8px; padding-top: 0; }
  /* On a phone a minute is twenty pixels, so the segments say what they
     are by colour alone; the words are in each one's title and the row's
     line under it. */
  .runs-seg { height: 22px; padding: 0; color: transparent; }
  .dag { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dag-step + .dag-step::before { display: none; }
  .measure-row { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  /* The three invariant figures are two panes side by side, and one column
     here: the repository over its file, the block over what was weighed,
     the recommendation over its fortnights. */
  .figure-scope, .figure-route, .figure-improve { grid-template-columns: minmax(0, 1fr); }
  .scope-list { grid-template-columns: 60px minmax(0, 1fr); }
  .improve-bar { grid-template-columns: 104px minmax(0, 1fr) 52px; }
  .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 was meant to be what says "there are
     more".

     It is not, and a stranger's first run is where that showed: the local
     edition's five pages end on Tools about four pixels short of the right
     edge, so the strip looks like the whole of itself and Connectors is a
     page nobody knows is there. A cut-off tab only says "there is more"
     when a tab happens to straddle the edge, and which one does depends on
     the page names.

     So the edge says it instead. The two fades are backgrounds rather than
     an overlay, because a background can be attached to the content
     (`local`) or to the element (`scroll`) and that difference is the whole
     trick: the raised-white pair scrolls away with the tabs, uncovering the
     dark pair, which does not - so each end is shaded exactly while there
     is something past it, and neither is shaded when the strip fits. No
     element, no JavaScript and nothing that has to be told the scroll
     moved. */
  .app-sidebar {
    padding: 6px 8px;
    border-right: none;
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    background:
      linear-gradient(to right, var(--bg-raised) 30%, transparent) left center / 28px 100% no-repeat local,
      linear-gradient(to left, var(--bg-raised) 30%, transparent) right center / 28px 100% no-repeat local,
      linear-gradient(to right, var(--shadow-strong), transparent) left center / 18px 100% no-repeat scroll,
      linear-gradient(to left, var(--shadow-strong), transparent) right center / 18px 100% no-repeat scroll,
      var(--bg-raised);
  }

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

  /* The git hosts are the one list whose last cell is a form rather than a
     fact (`.git-hosts-table` above). A token box beside a name in half a
     phone's width is two columns neither of which works, so here the name
     takes the line and the form takes the next one - the cell is kept, not
     dropped, because it is the only control the page has. */
  .git-hosts-table { grid-template-columns: minmax(0, 1fr); }

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

  /* ---- memory ---- */

  /* A field and its box take the width here; the desktop's 420px cap
     would leave a reason box half the screen wide. */
  .memory-field-inline { max-width: 100%; }

  /* The one cell a memory's row keeps is its scopes, and a scope is long
     enough to need the line rather than an ellipsis. */
  .memory-scroll .list-cell.keep { white-space: normal; overflow: visible; }

  /* The search box takes the line and the filters wrap under it. */
  .memory-search { flex-basis: 100%; max-width: 100%; }
  /* ---- end memory ---- */
}

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

/* --- memory --- */

/* Every panel on this page, and the kind chip on every row. Both carry a
   rule of their own rather than being bare hooks, so the no-eyes check -
   every class in the rendered DOM resolves to a rule - has nothing of this
   page's to report (codervibes-frontend). */
.memory-panel { min-width: 0; }
.memory-kind { white-space: nowrap; }

/* What a candidate or a memory says. It is the sentence a reader is
   deciding about, so it is the largest thing in its card and never
   truncated - a statement cut at a cell's edge is a decision made on half
   a sentence. */
.memory-statement {
  margin: 6px 0;
  font-size: 15px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.memory-statement-large { font-size: 17px; }

/* A scope: the string in monospace, with the name of the thing it points
   at beside it where the server found one. */
.memory-scope {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
}
.memory-scope .row-id { overflow-wrap: anywhere; }
.memory-scope-label { color: var(--text-dim); }
.memory-scopes { margin: 4px 0; }

.memory-none { color: var(--text-faint); font-size: 13px; }
.memory-when { color: var(--text-faint); font-size: 12px; margin-left: auto; }
.memory-contributors { margin: 4px 0; font-size: 13px; color: var(--text-dim); }
.memory-subheading {
  margin: 14px 0 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* A number that clicks through, and a link that is a button because it
   navigates inside the app. Both are the accent and underline on hover,
   the way a row's name does. */
.memory-count { font-variant-numeric: tabular-nums; }
.memory-count-link,
.memory-link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
}
.memory-count-link:hover,
.memory-link:hover { text-decoration: underline; }
.memory-link { font-family: var(--font-mono); font-size: 12px; }
.memory-sessions { display: inline-flex; flex-wrap: wrap; gap: 4px 10px; }
.memory-policy-line { margin: 4px 0; font-size: 13px; }

/* The forms. Every box on this page writes to the page's state and never
   redraws on a keystroke, so nothing here needs to survive a rebuild - but
   the labels do need to stack, since a label and its box side by side puts
   a two-line label beside a one-line field. */
.memory-form { display: grid; gap: 10px; margin-top: 10px; }
.memory-field { display: grid; gap: 4px; }
.memory-field-inline { max-width: 420px; }
.memory-field-label { font-size: 12px; font-weight: 600; color: var(--text-dim); }
.memory-textarea { width: 100%; font-family: inherit; resize: vertical; }
.memory-input,
.memory-select { width: 100%; }
.memory-tick { display: flex; align-items: baseline; gap: 6px; font-size: 13px; }
.memory-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
/* The app's `.danger-btn` pushes itself to the far edge, which is right for
   a row where it is an afterthought beside a name. Here it is one of four
   verdicts and one of the purge's two buttons, and a Reject sitting alone
   at the other end of a wide card reads as belonging to something else. */
.memory-actions .danger-btn { margin-left: 0; }
.memory-verdict { margin-top: 8px; }

/* The filter bars on the Memories, Contributions and Audit tabs. */
.memory-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px 16px;
  margin: 4px 0 12px;
}

/* The list's one search box, and the filters beside it. The filters are
   the console's pressable chips (.filter-chip), a size up: they are this
   page's only navigation, and each carries its count. */
.memory-search { flex: 1 1 320px; max-width: 520px; }
.memory-state-picks { display: flex; flex-wrap: wrap; gap: 6px; }
.memory-state-picks .filter-chip { font-size: 12px; padding: 4px 10px; }
.memory-list-body { min-width: 0; }

/* The list's columns. Every other list here splits its cells evenly; this
   one ends in a cell that holds Approve and Reject and, on a press, a line
   to type a reason into, and an even share of the width wrapped the
   buttons a syllable a line. The sentence gets the most, the verdict the
   next most, and the facts between them what a word needs. Under the
   table's minimum the box around it scrolls (.memory-scroll). */
.memory-list-table {
  grid-template-columns: minmax(240px, 3fr) 96px minmax(140px, 1.3fr) 116px minmax(120px, 1fr) 80px minmax(200px, 1.2fr);
  min-width: 992px;
}
.memory-list-table .memory-verdict button { white-space: nowrap; }

/* Approve and Reject, beside a pending row. The reason a reject needs
   opens under the two buttons rather than in a dialog: it is one line, and
   the row it is about should still be on screen while it is typed. */
.memory-verdict { display: grid; gap: 6px; min-width: 0; }
.memory-verdict-buttons { display: flex; gap: 6px; }
.memory-verdict-why { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.memory-verdict-reason { flex: 1 1 100%; min-width: 0; }

/* A table wider than the pane scrolls inside its own box; the page never
   scrolls sideways. Every table on this page is in one of these. */
.memory-scroll { overflow-x: auto; }

/* A memory's row: the statement takes the line, since it is the only part
   of a memory anybody recognises.

   Every other list on this console names a thing in two or three words, so
   `.list-name` and `.list-cell` cut at the column's edge with an ellipsis.
   Here the name is a whole sentence and the cells hold scope strings, and
   a memory cut at "Rate limits on the vendor's sandbox are per organ…" is
   a memory a reader cannot recognise, let alone decide about. So the rows
   of this page's tables wrap instead - scoped to `.memory-scroll`, which
   every table here sits in. */
.memory-scroll .list-name,
.memory-scroll .list-note,
.memory-scroll .list-cell {
  white-space: normal;
  text-overflow: clip;
  overflow-wrap: anywhere;
}
/* Wrapping, not spilling: the cells keep `overflow: hidden` from
   `.list-cell`, so a scope long enough to need two lines takes two lines
   inside its column rather than landing on the column beside it. */
.memory-scroll .memory-scope,
.memory-scroll .memory-scope .row-id { white-space: normal; }
.memory-scroll .list-row > * { align-items: flex-start; }
.memory-row-name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; min-width: 0; }
.memory-row-statement { overflow-wrap: anywhere; }
.memory-pin { flex: none; }

/* One memory's page. */
.memory-detail-head .memory-statement-large { margin: 6px 0 10px; }
.memory-stale-reason { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.memory-override { margin: 0 0 4px; font-size: 13px; color: var(--text-dim); }
.memory-lineage {
  padding: 8px 0;
  border-top: 1px solid var(--border);
}
.memory-lineage-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 4px;
}
/* A source's own words, marked as a quotation rather than as this page's
   prose: it is a transcript, and it must not read as something the product
   is asserting. */
.memory-excerpt {
  margin: 0;
  padding-left: 10px;
  border-left: 3px solid var(--border);
  color: var(--text-dim);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The policies, read as prose: the level and its sentence, the rules. */
.memory-levels { margin: 8px 0; display: grid; gap: 2px; }
.memory-level-name { display: flex; align-items: baseline; gap: 8px; }
.memory-level-words { margin: 0 0 6px; font-size: 13px; color: var(--text-dim); }
.memory-rules { display: grid; gap: 6px; }
.memory-rule {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
}
.memory-rule-name { font-weight: 600; }
.memory-rule-new {
  display: grid;
  gap: 8px;
  padding: 8px;
  border: 1px dashed var(--border);
  border-radius: 6px;
}
.memory-editor { margin-top: 12px; }
