/**
 * MFARM console — v2 "industrial" design system.
 *
 * Neutral graphite surfaces, 1px hairlines as the only stroke weight, near-square corners, and one
 * chromatic signal (accent purple) for primary actions and selection. Semantic green/amber/red/blue
 * are the only other colour, and they never carry state alone — every state in the markup pairs a
 * dot with a label and a timestamp.
 *
 * THREE SELF-HOSTED FAMILIES, and the register they carry is load-bearing: Bricolage Grotesque
 * for display, Instrument Sans for anything MFARM wrote to a human, JetBrains Mono for anything the
 * machine produced. The CSP still forbids external origins — the faces are served from this same
 * origin under `/fonts`, which cost exactly one directive (`font-src 'self'`) and no third party in
 * the load path. This file previously said "system mono only"; that stopped being true when the
 * three faces landed.
 *
 * EVERY TOKEN LIVES IN `design-tokens.css` NOW, not here. That file is linked before this one in
 * `index.html` and is shared with the React console at `/app`, which is the whole reason it is
 * separate: a token defined inside a component stylesheet is a token only that component can have.
 * Adding a colour, a radius or a duration means editing that file, never this one.
 *
 * The runtime knobs are consumed everywhere through var() WITH A LITERAL FALLBACK, so a stylesheet
 * that loads before the script still renders correctly and the four tweaks (accent, density,
 * liveness, nav width) apply without a re-render.
 */

/**
 * The nav-collapse knob is the ONE token that stays here, and the reason is scope: it is not a
 * design decision, it is this shell's own geometry. `design-tokens.css` is shared with a console
 * whose sidebar does not collapse to 56px, and shipping it a width for a rail it does not have
 * would be a token that describes nothing.
 */
:root[data-nav="icons"] { --mf-navw: 54px; --mf-navlabel: none; --mf-navshort: inline; }

/* ---------------------------------------------------------------------------- type */

/**
 * THE TYPE SCALE. The families and the metrics are in `design-tokens.css`; what follows is the
 * mapping from a role to the markup that wears it.
 *
 * THE `.t-*` CLASSES BELOW ARE A VOCABULARY, NOT THE APPLICATION. Nothing in the console wears one
 * yet, and for a while that meant the display face was SERVED AND NEVER RENDERED — 41 KB of
 * Bricolage Grotesque in the image that no rule referenced, which no test could see and which a
 * screenshot looked entirely fine without. It was caught by asking the deployed page which faces it
 * had actually loaded (`document.fonts` reported it `unloaded`).
 *
 * The display roles are applied to the EXISTING selectors further down instead, because those are
 * the elements that already exist and already carry the headings. Reach for a `.t-*` class when
 * adding new markup; do not add one to markup that a selector below already styles.
 *
 * Tracking is set alongside the `font:` shorthand rather than inside it, because the shorthand
 * resets `letter-spacing` to normal and cannot carry it — a display heading that quietly loses its
 * -3.5% is the single most common way this scale gets undone by a later edit.
 */
.t-body       { font: var(--ty-body); }

/* ---------------------------------------------------------------------------- reset */

*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

/**
 * THE DISPLAY FACE, on the five places that carry a heading.
 *
 * Bricolage Grotesque is for page titles, device names, dialog headings and stat numbers — 01's
 * list, and the reason the face is in the image at all. Grouped here rather than repeated at each
 * selector so that "which things are display type" is one answer in one place; the SIZES stay with
 * their own rules, because those are layout and were tuned against screens this change is not
 * re-laying-out.
 *
 * TRACKING IS SET ALONGSIDE, and it is the half that gets lost: a display face at 26px with default
 * tracking reads loose and slightly amateur, which is most of what separates typeset from styled.
 */
.headline,
.page-title,
.card > h2,
.card-title,
.dialog h2,
.stat .val {
  font-family: var(--f-display);
}
.page-title  { letter-spacing: var(--tr-display-lg); }
.card-title,
.card > h2,
.dialog h2 { letter-spacing: var(--tr-display-md); }
.stat .val { font-variant-numeric: tabular-nums; }

/**
 * THE DEFAULT IS SANS NOW, and that is the single highest-leverage line in this file.
 *
 * The console used to set `--mono` on `<body>`, so every sentence MFARM wrote to a human inherited
 * a monospace face. That inverts the register rule the whole system rests on: mono is how the
 * reader knows a string came out of the machine, and when the prose is mono too, the signal carries
 * nothing. Machine text now opts IN through `.mono` and `code`.
 */
body {
  margin: 0;
  background: var(--s-app, #08080A);
  color: var(--t-body, #D4D4D4);
  font-family: var(--f-ui);
  font-size: 13px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, p, dl, dd, figure { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }

button, input, select, textarea {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
}

button { cursor: pointer; background: none; border: none; padding: 0; }
button[disabled] { cursor: not-allowed; opacity: .45; }

/* Machine text opts in. Always selectable — a value you cannot select is a value you cannot use. */
code, .mono { font-family: var(--f-mono); font-size: 11.5px; user-select: text; }

[hidden] { display: none !important; }

/**
 * The focus ring, and it is NEVER SUPPRESSED anywhere in this stylesheet.
 *
 * `--b-focus` is its own token rather than the accent, because in light theme the accent is dark
 * enough that a 2px ring around a dark button vanishes into it. Offset 2px so the ring sits outside
 * a filled control rather than eating its first two pixels of padding.
 */
:focus-visible {
  outline: 2px solid var(--b-focus, #8A74D6);
  outline-offset: 2px;
}

::selection { background: var(--mf-accent-soft, rgba(117,94,184,.16)); }

/* Live counters must not reflow as digits change. */
.tnum, .clock, .wait, .hb { font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------- motion */

@keyframes mf-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@keyframes mf-bar   { 0% { transform: translateX(-100%); } 100% { transform: translateX(320%); } }
@keyframes mf-spin  { to { transform: rotate(360deg); } }

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

/**
 * SCROLLBARS, drawn as a thumb floating in the surface rather than a gutter beside it — the 3px
 * border in the page ground is what makes a 10px bar read as 4px.
 *
 * `-webkit-` only, on purpose. Chrome honours the standard `scrollbar-color` when it is set and then
 * ignores every `::-webkit-scrollbar` rule, so setting both would lose the width everywhere Chrome
 * runs. Firefox, which has only the standard property, gets it through `@supports`.
 */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--b-field, #242427);
  border: 3px solid var(--s-app, #08080A);
  border-radius: var(--r-pill, 999px);
}
@supports not selector(::-webkit-scrollbar) {
  html { scrollbar-color: var(--b-field, #242427) transparent; }
}

/* ---------------------------------------------------------------------------- primitives */

/* Micro-label: the uppercase 10px key that titles every block in this system. */
.micro {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--t-micro, #5A5A5A);
}

.label { font-size: 11.5px; color: var(--t-label, #8A8A8A); }
.help  { font-size: 11.5px; color: var(--t-help, #787878); }
.caption { font-size: 11px; color: var(--t-caption, #6A6A6A); }
.secondary { color: var(--t-secondary, #C4C4C4); }

/**
 * The icon element itself.
 *
 * `flex: 0 0 auto` so an icon never shrinks when the label beside it is long — a squashed 16px
 * glyph is the tell that an icon set was retrofitted. `vertical-align` because an inline icon sits
 * on the text baseline and Lucide's 24px box has its optical centre slightly above it.
 *
 * NO COLOUR OF ITS OWN. `stroke: currentColor` is set on the element in `icons.js`, so an icon
 * takes the colour of the text it sits with and can never disagree with its label — which is also
 * why an icon in this console never carries meaning on its own.
 */
.icon {
  flex: 0 0 auto;
  vertical-align: -.15em;
}

/* Status dot. Never alone — the markup always pairs it with a word. */
.dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--t-micro, #5A5A5A);
  flex: 0 0 auto;
  display: inline-block;
}
.dot.ok    { background: var(--ok-dot, #6FCB63); }
.dot.warn  { background: var(--warn, #E8A30E); }
.dot.bad   { background: var(--bad-dot, #E0505F); }
.dot.info  { background: var(--info, #7FA0D8); }
.dot.accent{ background: var(--mf-accent, #755EB8); }
.dot.live  { animation: mf-pulse var(--mf-pulse, 2.2s) ease-in-out infinite; }

/* Status pill. */
/**
 * Status pill. Dot + label, always, and a timestamp ADJACENT rather than inside.
 *
 * The timestamp sits beside the pill rather than within it for a layout reason and a meaning one.
 * A pill that grows to hold `2 minutes ago` stops being a fixed-width token you can scan down a
 * column of; and the pill states a CONDITION while the timestamp states WHEN IT WAS OBSERVED,
 * which are two facts that age differently — a stale poll invalidates the second without touching
 * the first.
 *
 * `--r-pill` now, not the old 2px. 06 gives pills and chips the pill radius and everything else the
 * ordinary scale, which is what makes a status token read as a token rather than as a small button.
 */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 9px;
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: var(--r-pill, 999px);
  font-family: var(--f-mono);
  font-size: 9px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--t-label, #8A8A8A);
  white-space: nowrap;
}
/**
 * Pill + timestamp, laid out as one unit.
 *
 * The timestamp is a SIBLING of the pill rather than a child, so the pill keeps the fixed width
 * that makes a column of them scannable — and so the two can age independently, which they do: the
 * pill says what the condition is, the timestamp says when we last saw it be that.
 */
.pill-row { display: inline-flex; align-items: baseline; gap: 7px; white-space: nowrap; }
.pill-at  { font-family: var(--f-mono); font-size: 10.5px; color: var(--t-caption, #6A6A6A); }

.pill.ok     { color: var(--ok-text, #7FD873);  background: var(--ok-bg);  border-color: var(--ok-line); }
.pill.warn   { color: var(--warn, #E8A30E);     background: var(--warn-bg); border-color: var(--warn-line); }
.pill.bad    { color: var(--bad-text, #EE7C88); background: var(--bad-bg); border-color: var(--bad-line); }
.pill.info   { color: var(--info, #7FA0D8);     background: var(--info-bg); border-color: var(--info-line); }
.pill.accent { color: var(--accent-text, #C9BCF0); background: var(--mf-accent-soft); border-color: var(--mf-accent-line); }

/**
 * Capability chip.
 *
 * A capability the device does NOT declare renders struck through, present and dimmed — never
 * removed. Filtering an absence out of the list is how a console starts lying by omission: the
 * reader cannot tell "this device cannot do it" from "nobody thought to show it", and the second
 * reading is the one that sends somebody to debug a feature that was never there.
 *
 * THE ✓ AND × GLYPHS ARE GONE, and their absence is the design decision. FILL AND STRIKE ALREADY
 * CARRY IT: a filled chip is present, a transparent struck one is not, and adding a tick to the
 * first said the same thing twice while adding a cross to the second competed with the strike for
 * the same meaning. It also makes the chip a capability NAME rather than a name plus punctuation,
 * which matters because the name is a machine string somebody may need to copy.
 */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px;
  background: var(--s-chip, #191919);
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: var(--r-pill, 999px);
  font-family: var(--f-mono);
  font-size: 10.5px;
  color: var(--t-secondary, #C4C4C4);
}
.chip.no {
  color: var(--t-strike, #4A4A4A);
  background: transparent;
  border-color: var(--b-row, #151517);
  text-decoration: line-through;
  /* The strike must read as deliberate rather than as a rendering artefact at 10.5px. */
  text-decoration-thickness: 1px;
}

/* ============================================================ device detail (document 05 §03) ==
 *
 * The operator's page. Its whole job is to state what is wrong, what has been tried, and precisely
 * what an action will and will not do — so the two things it adds to the shared vocabulary are a
 * card that is visibly a WARNING rather than a section, and a list that is visibly a CONTRACT.
 */

/* Identity under the page title: what kind of thing this is, then how to name it in a log line. */
.ident {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
  margin: -6px 0 18px;
}
.ident .mono { font-size: 11px; color: var(--t-caption, #6A6A6A); }
.badge {
  padding: 3px 8px;
  border-radius: var(--r-pill, 999px);
  background: var(--info-bg);
  border: 1px solid var(--info-line);
  font-family: var(--f-mono);
  font-size: 9px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--info, #7FA0D8);
}

/*
 * THE GATE. A quarantine card is not a section of the page, it is the reason the page was opened,
 * so it carries the bad tint on its own surface rather than relying on a pill to signal it.
 */
.card.gate {
  background: var(--bad-bg, rgba(224, 80, 95, .07));
  border-color: var(--bad-line, rgba(224, 80, 95, .30));
  border-radius: var(--r-lg, 12px);
  padding: 18px 20px;
}
.card.gate .card-title { color: var(--bad-text, #EE7C88); font-size: 15px; }
.card.gate .help { max-width: 62ch; color: var(--t-secondary, #C4C4C4); }
.card.gate .help code, .card.gate .help q {
  font-family: var(--f-mono); font-size: 11px; color: var(--t-secondary, #C4C4C4);
}

/*
 * The consequence list, on the darkest surface the page has.
 *
 * It sits INSIDE the tinted gate on a plain ground, which is the whole visual argument: the tint
 * says "something is wrong", and the panel inside it says "here is exactly what pressing the button
 * does". Reversing those — a list tinted red — would make the contract read as another alarm.
 */
.consequence {
  margin-top: 14px;
  padding: 14px 15px;
  border-radius: var(--r-md, 9px);
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-card, #1F1F22);
}
.csq-head {
  font-size: 11.5px; font-weight: 500;
  color: var(--t-primary, #F1F1F1);
  margin-bottom: 10px;
}
.csq-list { list-style: none; margin: 0 0 13px; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.csq { display: flex; gap: 8px; align-items: flex-start; }
.csq-mark { flex: none; font-family: var(--f-mono); font-size: 10px; line-height: 1.6; }
.csq.yes .csq-mark { color: var(--ok-text, #7FD873); }
.csq.no  .csq-mark { color: var(--bad-text, #EE7C88); }
.csq-body { font-size: 11.5px; line-height: 1.5; color: var(--t-help, #787878); }
/* The emphasis is on the WORD — "one", "not" — so the sentence still reads correctly in mono, in
 * a screen reader, and to anyone who cannot tell the arrow from the cross by colour. */
.csq-body strong { color: var(--t-secondary, #C4C4C4); font-weight: 600; }
.csq-note {
  padding: 9px 11px;
  border-radius: var(--r-sm, 6px);
  background: var(--ok-bg, rgba(111, 203, 99, .06));
  border: 1px solid var(--ok-line, rgba(111, 203, 99, .24));
  font-size: 11px;
  line-height: 1.45;
  color: var(--ok-text, #7FD873);
}

/* ================================================ the cockpit's four states (document 04) ======
 *
 * S3's queued mark, S4's accounting, and the unresolved frame a queued session draws.
 */

/*
 * A PULSE, NOT A BAR. `.dot.live` means "a stream is arriving"; this is the opposite — nothing is
 * arriving, and that is expected. It has no extent, so it cannot be read as a position along a
 * journey the way a 32% sliver sweeping left to right could.
 *
 * 2.2s is the system loop from document 04, the same one every "we are waiting on a heartbeat"
 * signal breathes on, so two of them on screen at once stay in step.
 */
@keyframes mf-breathe { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
/*
 * Through `--mf-pulse`, like `.dot.live` above it, so BOTH gates apply: the console's own
 * `data-liveness="still"` and the platform's `prefers-reduced-motion` each set that variable to 0s.
 * A hardcoded 2.2s here would be a second pulse the settings could not reach — and the one animation
 * a person who turned motion off would still be watching.
 */
.dot.breathe { animation: mf-breathe var(--mf-pulse, 2.2s) ease-in-out infinite; }

/* What the session left behind. Four numbers, tabular, no card chrome between them. */
.card.ended { margin-top: var(--mf-gap, 14px); }
/*
 * The sentence that makes the numbers mean something — document 04 S4 leads S4 with it. It used to
 * be rendered over the panel, which stopped being tenable when the ended stage settled to a third
 * of its height: a paragraph about who released the session does not go inside a 300px phone.
 */
.card.ended .ended-lead { margin-bottom: 12px; }

/*
 * The waiting block borrows the gate's shape and not its colour. `.gate` is the red tint that says
 * something is wrong; a queue is not wrong, it is simply not yet — so the tint goes amber and the
 * layout stays, because both are "read this before you look at anything else on the page".
 */
.card.gate.waiting {
  background: var(--warn-bg, rgba(232, 163, 14, .06));
  border-color: var(--warn-line, rgba(232, 163, 14, .24));
  margin-bottom: var(--mf-gap, 14px);
}
.card.gate.waiting .card-title { color: var(--warn, #E8A30E); }
/* A card head rendered as a `micro` label rather than a title still needs the colour. */
.card.gate.waiting > .card-head .micro { color: var(--warn, #E8A30E); }

/* A stat whose number is bad news, marked on the edge rather than only by its dot. */
.card.stat-bad { border-color: var(--bad-line, rgba(224, 80, 95, .30)); }
.endstats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 14px 20px;
}
/*
 * EACH NUMBER IN ITS OWN BOX — document 04 S4. They were a flat grid, which reads as one paragraph
 * of four numbers; boxed, each is a separate answer to a separate question, which is what they are.
 */
.endstat {
  display: flex; flex-direction: column; gap: 3px; min-width: 0;
  padding: 12px 14px;
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: var(--r-md, 9px);
  background: var(--s-inset, #050506);
}
.endstat-v {
  font-family: var(--f-mono);
  font-size: 17px;
  color: var(--t-primary, #F1F1F1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.endstat-l { font-size: 10.5px; color: var(--t-caption, #6A6A6A); }

/*
 * "Everything else works" — document 04 S2 gives this its own box, because it is the half that
 * decides whether a stream-less device is worth keeping. Amber like the state above it: not a
 * fault, but the reason the panel is empty.
 */
.worksbox {
  margin-top: 12px;
  padding: 11px 13px;
  border-radius: var(--r-sm, 6px);
  background: var(--warn-bg, rgba(232, 163, 14, .06));
  border: 1px solid var(--warn-line, rgba(232, 163, 14, .24));
  text-align: left;
  max-width: 40ch;
}
.worksbox-h {
  font-size: 11.5px;
  font-weight: 500;
  color: var(--warn, #E8A30E);
  margin-bottom: 5px;
}
/* The struck capability, wherever it is NAMED in prose — one visual for one fact, the same as the
   chip on device detail and the control in the rail. */
.dev-overlay s, .help s { text-decoration-thickness: 1px; color: #8A8A8A; }

/*
 * THE QUEUED FRAME IS PRESENT BUT UNRESOLVED — document 04. It has not been allocated, so it is
 * not yet a real object: blurred, dim, and flat. Nothing about it firms up until a device is
 * claimed, which is also the first beat of the bring-up choreography.
 */
.devpanel[data-resolved="no"] .dev-fit {
  filter: blur(9px);
  opacity: .38;
  transform: scale(.955);
}
/*
 * `--m-arrive` and `--ease-emphatic` from the stage-1 motion scale, not a literal 700ms: both
 * motion gates collapse the durations to 1ms through those variables, and a hardcoded number here
 * would keep animating for somebody who had switched motion off. This is also beat 01 of the
 * bring-up choreography — chassis resolving out of blur — so it is written once and reused there.
 */
.devpanel[data-resolved="no"] .dev-fit,
.devpanel[data-resolved="yes"] .dev-fit {
  transition: filter var(--m-arrive, 700ms) var(--ease-emphatic),
              opacity var(--m-arrive, 700ms) var(--ease-emphatic),
              transform var(--m-arrive, 700ms) var(--ease-emphatic);
}

/*
 * The fleet row's name block, which is the row's link. A button, so it is reachable by keyboard and
 * announced as an action — styled back down to look like the text it replaced, because a row of
 * four buttons that look like buttons is a toolbar, not a table.
 */
.fleet-open {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: var(--r-sm, 6px);
}
.fleet-open:hover .fleet-name { color: var(--accent-text, #C9BCF0); }
.fleet-open:focus-visible { outline: 2px solid var(--b-focus); outline-offset: 3px; }

/* Key/value row. */
.kv { display: grid; grid-template-columns: minmax(96px, 34%) 1fr; }
.kv dt, .kv dd {
  padding: 9px 0;
  border-top: 1px solid var(--b-row, #151517);
  min-width: 0;
}
.kv dt { color: var(--t-label, #8A8A8A); font-size: 11.5px; }
.kv dd { color: var(--t-body, #D4D4D4); overflow-wrap: anywhere; }
.kv dt:first-of-type, .kv dt:first-of-type + dd { border-top: none; }

/* Mono value with a copy button. */
/**
 * Copy field. Machine text is ALWAYS in one of these.
 *
 * Truncation is by ellipsis and never by shortening the data: `3b97a8de…` that scrolls is a value
 * you can still select and paste, while `3b97a8de` rendered as the whole id is a value that looks
 * complete and is not. That distinction is the reason this primitive exists rather than a `<code>`.
 */
.copyrow {
  display: flex; align-items: center; gap: 8px;
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-field, #242427);
  border-radius: var(--r-md, 9px);
  padding: 8px 8px 8px 10px;
  min-width: 0;
}
/*
 * ELLIPSIS, NEVER A SHORTENED VALUE — document 06's copy-field spec.
 *
 * This scrolled instead, with the scrollbar hidden: the overflow was reachable but there was no
 * sign it existed, so a truncated uuid looked like a complete one. An ellipsis is the cue that
 * there is more. The DATA is untouched either way — the full value is in the DOM, selectable, and
 * the Copy button takes all of it — which is the half of the rule that actually matters.
 */
.copyrow code {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--t-body, #D4D4D4);
  font-size: 11.5px;
  scrollbar-width: none;
}
.copyrow code::-webkit-scrollbar { display: none; }

/*
 * A block somebody is going to paste into their own suite.
 *
 * SCROLLS, NEVER WRAPS. A wrapped line of code changes what the code appears to say — an
 * indentation level that is really a soft wrap is how a person ends up debugging their own eyes —
 * so the overflow goes sideways in its own box and the page body never moves with it.
 *
 * `user-select` is left alone rather than set to `all` the way `.keyplain` does. A key is copied
 * whole or not at all; a snippet is read, and a reader wants to select the one line they are
 * changing. The Copy button beside it covers the whole-block case.
 */
.snippet {
  margin: 0;
  overflow-x: auto;
  padding: 11px 12px;
  border-radius: var(--r-md, 9px);
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-field, #242427);
  font-family: var(--f-mono);
  font-size: 11.5px;
  line-height: 1.65;
  color: var(--t-body, #D4D4D4);
  white-space: pre;
  tab-size: 2;
}
/* The comments carry the traps — each one is a mistake somebody already made — so they are toned
   down rather than greyed out of legibility, which is what a syntax theme does to them. */
.snippet .cmt { color: var(--t-caption, #6A6A6A); }

/**
 * Buttons. Three variants and no more: primary, secondary, destructive.
 *
 * h32 / pad 8-12 / r9 at comfortable density, per 06. ONE PRIMARY PER SCREEN — that is a rule the
 * stylesheet cannot enforce and the screens have to keep, and it is what makes the accent mean
 * "this is the thing to do" rather than "this is a button".
 *
 * The destructive variant is only ever reached INSIDE A CONFIRM DIALOG. A bare red row action is
 * the pattern where somebody destroys a device from a table they were scrolling.
 */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 32px;
  padding: 8px 12px;
  background: var(--s-btn, #131315);
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: var(--r-md, 9px);
  color: var(--t-body, #D4D4D4);
  font: var(--ty-strong);
  white-space: nowrap;
  transition: border-color var(--m-fast, 160ms) var(--ease-out), background var(--m-fast, 160ms) var(--ease-out);
}
.btn:active:not([disabled]) { transform: translateY(.5px); transition-duration: var(--m-instant, 90ms); }
/* An <a> dressed as a button (downloads, exports) reads as one: the UA's link underline is not ours. */
a.btn, a.btn:hover { text-decoration: none; }
.btn:hover:not([disabled]) { border-color: var(--b-hover-strong, #454548); color: var(--t-primary, #F1F1F1); }

.btn.primary {
  background: var(--mf-accent, #755EB8);
  border-color: var(--mf-accent, #755EB8);
  color: var(--on-accent, #fff);
}
.btn.primary:hover:not([disabled]) { background: var(--accent-hover, #8570C9); border-color: var(--accent-hover, #8570C9); }

.btn.danger {
  background: transparent;
  border-color: var(--bad-outline, #4A2436);
  color: var(--bad-text, #EE7C88);
}
.btn.danger:hover:not([disabled]) { border-color: var(--bad-dot, #E0505F); }

.btn.danger-solid { background: var(--bad-solid, #B32333); border-color: var(--bad-solid, #B32333); color: #fff; }
.btn.danger-solid:hover:not([disabled]) { background: var(--bad-solid-hover, #C93343); border-color: var(--bad-solid-hover, #C93343); }

.btn.ghost { background: transparent; }
.btn.tiny { min-height: 24px; padding: 4px 8px; font-size: 11px; border-radius: var(--r-sm, 6px); }
.btn.wide { width: 100%; }

kbd {
  display: inline-block;
  padding: 1px 5px;
  background: var(--hover-soft);
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: 2px;
  font-family: var(--f-mono);
  font-size: 10px;
  color: var(--t-label, #8A8A8A);
}
.btn.primary kbd { background: rgba(0, 0, 0, .22); border-color: rgba(255, 255, 255, .22); color: rgba(255, 255, 255, .82); } /* on a filled accent in BOTH themes — the surface under this is dark either way */

/* Fields. */
.field {
  width: 100%;
  height: 38px;
  padding: 0 11px;
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-field, #242427);
  border-radius: 2px;
  color: var(--t-primary, #F1F1F1);
}
.field::placeholder { color: var(--t-micro, #5A5A5A); }
.field:focus { border-color: var(--mf-accent, #755EB8); outline: none; }
.field.tall { height: 42px; }

/* Card. Borders, never shadow. */
.card {
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: 3px;
  padding: var(--mf-cp, 15px);
  min-width: 0;
}
.card > h2, .card-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--t-primary, #F1F1F1);
}
.card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 11px;
}
.card.flush { padding: 0; }
.card.flush .card-head { padding: var(--mf-cp, 15px); margin: 0; border-bottom: 1px solid var(--b-row, #151517); }

.inset {
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 2px;
  padding: 11px;
}

/* Indeterminate progress — a queued action has no percentage to report, and inventing one lies. */
.bar {
  position: relative;
  height: 3px;
  background: var(--s-chip, #191919);
  border-radius: 2px;
  overflow: hidden;
}
.bar > i { display: block; height: 100%; background: var(--mf-accent, #755EB8); border-radius: 2px; }
.bar.indet > i { width: 32%; animation: mf-bar 1.4s linear infinite; }
.bar.ok > i { background: var(--ok-dot, #6FCB63); }
.bar.bad > i { background: var(--bad-dot, #E0505F); }
.bar.lease { height: 4px; }

.spinner {
  width: 13px; height: 13px;
  border: 1.5px solid var(--b-btn, #2B2B2E);
  border-top-color: var(--mf-accent, #755EB8);
  border-radius: 50%;
  animation: mf-spin 1s linear infinite;
}

/* Event timeline: 6px dot with a connector drawn by the ::before of the item, not a border on the
   list, so the last item's connector stops at its own dot. */
.timeline > li {
  position: relative;
  padding: 0 0 14px 18px;
}
.timeline > li:last-child { padding-bottom: 0; }
.timeline > li::before {
  content: "";
  position: absolute; left: 0; top: 5px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--t-micro, #5A5A5A);
}
.timeline > li::after {
  content: "";
  position: absolute; left: 2.5px; top: 14px; bottom: 2px;
  width: 1px;
  background: var(--b-inset, #1A1A1C);
}
.timeline > li:last-child::after { display: none; }
.timeline > li.ok::before   { background: var(--ok-dot, #6FCB63); }
.timeline > li.warn::before { background: var(--warn, #E8A30E); }
.timeline > li.bad::before  { background: var(--bad-dot, #E0505F); }
.timeline > li.info::before { background: var(--info, #7FA0D8); }
.timeline > li.accent::before { background: var(--mf-accent, #755EB8); }
.tl-title { color: var(--t-body, #D4D4D4); }
.tl-note  { font-size: 11px; color: var(--t-caption, #6A6A6A); }

/**
 * Empty state: a sentence saying WHAT IS ABSENT, then the one thing to do about it.
 *
 * Never a spinner and never a skeleton where an explanation belongs. A skeleton says "this is
 * loading" whether or not it is, so it is the one loading affordance that can be a lie — and in a
 * console whose whole claim is that it does not lie about the machine, the empty state is where
 * that claim is cheapest to keep and most often broken.
 */
.empty {
  padding: 26px 16px;
  text-align: center;
  color: var(--t-help, #787878);
  font-size: 12px;
  text-wrap: pretty;
}
.empty strong {
  display: block;
  color: var(--t-secondary, #C4C4C4);
  font: var(--ty-display-md);
  letter-spacing: var(--tr-display-md);
  margin-bottom: 6px;
}

.error-text { color: var(--bad-text, #EE7C88); font-size: 11.5px; }
.ok-text    { color: var(--ok-text, #7FD873); }
.warn-text  { color: var(--warn, #E8A30E); }
.bad-text   { color: var(--bad-text, #EE7C88); }

.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.row.tight { gap: 6px; }
.row.between { justify-content: space-between; }

/**
 * A `.row.between` whose left side must SHRINK rather than push the right side onto its own line.
 *
 * TWO PROPERTIES, AND `min-width: 0` ALONE IS NOT ENOUGH — which cost a round trip to find out.
 * `.row` sets `flex-wrap: wrap`, and in flex layout WRAPPING IS DECIDED BEFORE SHRINKING: once the
 * left column's max-content plus the button exceeds the line, the button moves to its own line and
 * the column is never asked to narrow. So the row must be told not to wrap FIRST; only then does
 * `min-width: 0` let the column shrink past its content (a flex item defaults to `min-width: auto`,
 * which refuses to).
 *
 * The symptom is a list that looks broken because one person's email address was longer than
 * another's: neighbouring entries put their control in different places. Seen on the share dialog's
 * link list, in a browser — no test in this repo renders a dialog wide enough to have caught it.
 */
.row.between.fit { flex-wrap: nowrap; }
.row.between.fit > .shrink { min-width: 0; }
.row.between.fit > .shrink > * { overflow-wrap: anywhere; }
.spacer { flex: 1 1 auto; }
.stack { display: grid; gap: 10px; }
.stack.tight { gap: 6px; }

/* ---------------------------------------------------------------------------- sign in
 *
 * MOVED TO `signin.css`. The sign-in screen is now the design from
 * `Farm_app_design_exploration/Sign In.dc.html` — a full-bleed dark landing surface with its
 * own layout, its own keyframes and not one rule in common with the two-column form that used
 * to live here. Keeping the old block would leave ~80 lines of CSS that nothing selects, which
 * reads as load-bearing to the next person and quietly is not.
 */

.brand { display: inline-flex; align-items: center; gap: 9px; }
.brand-mark {
  display: grid; place-items: center;
  width: 22px; height: 22px;
  background: var(--mf-accent, #755EB8);
  border-radius: var(--r-sm, 6px);
  color: var(--on-accent, #fff);
  font: 700 12px/1 var(--f-display);
}
.brand-word {
  font-family: var(--f-display);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: var(--tr-display-lg, -.02em);
  color: var(--t-primary, #F1F1F1);
}
.brand-mark.lg { width: 30px; height: 30px; font-size: 16px; border-radius: 3px; }

/* ---------------------------------------------------------------------------- shell */

/**
 * THE NAV AND THE TOPBAR DO NOT SCROLL. ONLY THE CONTENT DOES.
 *
 * `min-height: 100%` made the whole DOCUMENT the scroll container, so the sidebar and the farm
 * status bar scrolled away with it. Measured on the deployed console: the session screen is 2695px
 * against an 813px viewport, so a person reading the log or the steps had scrolled 1900px and no
 * longer had Fleet, Runs, Health or the "0 of 5 ready" line anywhere on screen — on the one screen
 * where "is the farm even up?" is the next question they will ask.
 *
 * `height` rather than `min-height` fixes the shell to the viewport; `1fr` gives the remaining
 * space to the two panes, and each scrolls itself. `100dvh` so mobile browser chrome appearing does
 * not push the last row out of reach, with the `%` line before it for anything that does not know
 * the unit — `html, body { height: 100% }` is already set above.
 */
.shell {
  height: 100%;
  height: 100dvh;
  /* Below this the cockpit's two panes stop being panes; the shell scrolls as a whole instead of
     letting them overlap. */
  min-height: 560px;
  overflow: hidden;
  background: var(--s-app, #08080A);
  display: grid;
  /* `minmax(0, …)` on the content track, not `1fr`: a `1fr` track's minimum is its content's, so one
     long unbreakable log line widened the whole page past the window. */
  grid-template-columns: var(--mf-navw, 198px) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas: "head head" "nav main";
}

.topbar {
  grid-area: head;
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
  min-height: 50px;
  padding: 0 12px;
  background: var(--s-chrome, #050506);
  border-bottom: 1px solid var(--b-chrome, #191919);
}
.topbar .divider { width: 1px; height: 20px; background: var(--b-card, #1F1F22); }

/* Farm status — one pill of segments, and a button, because clicking it opens Health. */
.farmstat {
  display: flex; align-items: center;
  min-width: 0;
  padding: 3px 4px 3px 10px;
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: var(--r-pill, 999px);
  font-size: 12px;
  line-height: 20px;
  white-space: nowrap;
  color: var(--t-label, #8A8A8A);
}
.farmstat:hover { border-color: var(--b-hover, #3A3A3D); }
.farmstat b { color: var(--t-primary, #F1F1F1); font-weight: 600; }
.farmstat .seg { display: inline-flex; align-items: center; gap: 6px; padding: 0 9px; }
.farmstat .seg:first-child { padding-left: 0; }
.farmstat .seg + .seg { border-left: 1px solid var(--b-card, #1F1F22); }
.farmstat .dot { width: 6px; height: 6px; }
/* A powered-on host is money leaving whether or not anything is allocated: amber, the tone its
   Infrastructure nav dot already wears for the same fact. */
.farmstat .seg-host.warn { color: var(--warn, #E8A30E); }

.palette-btn {
  display: flex; align-items: center; gap: 10px;
  flex: 0 1 260px;
  min-width: 140px;
  max-width: 300px;
  padding: 5px 6px 5px 11px;
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-field, #242427);
  border-radius: var(--r-md, 9px);
  color: var(--t-help, #787878);
  font-size: 12.5px;
  white-space: nowrap;
}
.palette-btn > span:first-child { overflow: hidden; text-overflow: ellipsis; }
.palette-btn:hover { border-color: var(--b-hover, #3A3A3D); }
.palette-btn kbd {
  padding: 0 5px;
  background: var(--s-chip, #191919);
  border: 1px solid var(--b-field, #242427);
  border-radius: var(--r-xs, 4px);
  font: 400 10.5px/16px var(--f-mono);
  color: var(--t-label, #8A8A8A);
}

.avatar {
  display: grid; place-items: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--mf-accent-soft, rgba(117,94,184,.16));
  border: 1px solid var(--mf-accent-line, rgba(117,94,184,.42));
  color: var(--t-primary, #F1F1F1);
  font-size: 10px;
  flex: 0 0 auto;
}
.avatar.sq { border-radius: 2px; }

.who { display: flex; align-items: center; gap: 9px; font-size: 12px; }
.who .avatar {
  width: 24px; height: 24px;
  background: var(--s-chip, #191919);
  border-color: var(--b-field, #242427);
  font-size: 10.5px;
}
/* Two lines where one used to be: who, and which tenant their work belongs to. */
.who .stack { line-height: 1.3; }
#who-email { color: var(--t-secondary, #C4C4C4); }
#who-org { color: var(--t-caption, #6A6A6A); font-size: 10.5px; white-space: nowrap; }
.link { color: var(--t-label, #8A8A8A); font-size: 11.5px; }
.link:hover { color: var(--t-primary, #F1F1F1); }

/**
 * THE TOP BAR GIVES THINGS UP IN THE ORDER THEY MATTER LEAST.
 *
 * Media queries, because these ARE measured against the window. The cockpit's one-column switch is
 * not, and is a container query on the workspace instead — the sidebar's width changes the space it
 * has, and a window query cannot see that.
 *
 * NOTHING HERE HIDES A CAPABILITY. The search trigger goes below 760px and ⌘K still opens the
 * palette; the who block goes below 980px and the palette carries Sign out.
 */
@media (max-width: 1239.98px) { .who .stack, #build { display: none; } }
@media (max-width: 1179.98px) { .farmstat .seg-extra { display: none; } }
@media (max-width: 979.98px)  { .who { display: none; } }
@media (max-width: 759.98px)  { .palette-btn { display: none; } }

/* -------------------------------------------------------------------- sidebar */

.sidebar {
  grid-area: nav;
  display: flex; flex-direction: column;
  gap: 2px;
  padding: 10px 8px;
  background: var(--s-nav, #0A0A0C);
  border-right: 1px solid var(--b-chrome, #191919);
  min-width: 0;
  overflow-x: hidden;
  /* Its own scroller, so a short viewport can still reach Settings and the collapse control rather
     than clipping them — the nav is nine items and a footer, so this almost never engages. */
  overflow-y: auto;
}
.navgroup {
  margin: 12px 0 6px;
  padding: 0 9px;
  font: 500 10px/10px var(--f-ui);
  text-transform: uppercase; letter-spacing: var(--tr-label, .1em);
  color: var(--t-navhead, #484848);
}
.navgroup:first-child { margin-top: 0; }
/* The label and its collapsed stand-in, a middle dot — so the groups stay visibly groups at 54px. */
.navgroup .txt   { display: var(--mf-navlabel, inline); }
.navgroup .short { display: var(--mf-navshort, none); }

.navitem {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 8px 9px;
  border-radius: var(--r-md, 9px);
  color: var(--t-nav, #ADADAD);
  font-size: 13px;
  font-weight: 500;
  line-height: 16px;
}
.navitem:hover { background: var(--hover-soft); color: var(--t-primary, #F1F1F1); }
.navitem .glyph,
.navtoggle .glyph {
  display: inline-grid; place-items: center;
  width: 16px; height: 16px;
  flex: 0 0 auto;
  color: var(--t-label, #8A8A8A);
}
.navitem .txt { display: var(--mf-navlabel, block); white-space: nowrap; overflow: hidden; }
.navitem .sc  { display: var(--mf-navlabel, block); margin-left: auto; font: 400 9.5px/1 var(--f-mono); color: var(--t-micro, #5A5A5A); }
/* `--t-primary`, NOT white. The ground here is `--mf-accent-soft`, a 10% tint of the accent over
   the nav surface — near-black in dark, near-white in light. White text on it is correct in one
   theme and invisible in the other, which is exactly the bug stage 8 exists to find. */
.navitem.is-active {
  background: var(--mf-accent-soft, rgba(117,94,184,.16));
  color: var(--t-primary, #F1F1F1);
  /* The selection edge. A tint alone is the same weight as a hover at a glance. */
  box-shadow: inset 2px 0 0 var(--mf-accent, #755EB8);
}
.navitem.is-active .glyph { color: var(--mf-accent, #755EB8); }

.navtoggle {
  display: flex; align-items: center; gap: 10px;
  margin-top: 6px;
  padding: 8px 9px;
  border-radius: var(--r-md, 9px);
  color: var(--t-micro, #5A5A5A);
  font-size: 12px;
  text-align: left;
}
.navtoggle .txt { display: var(--mf-navlabel, block); }
.navtoggle:hover { color: var(--t-secondary, #C4C4C4); background: var(--hover-soft); }

.sessioncard {
  margin-top: 8px;
  padding: 10px;
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: var(--r-lg, 12px);
}
#sc-lease { font: 400 11px/17px var(--f-mono); color: var(--t-caption, #6A6A6A); }
#sc-open { border-radius: 7px; }
:root[data-nav="icons"] .sessioncard { display: none; }
.sessionstub {
  display: none;
  width: 38px; height: 38px;
  margin: 8px auto 0;
  place-items: center;
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: var(--r-md, 9px);
}
:root[data-nav="icons"] .sessionstub { display: grid; }

/**
 * BELOW 900px THE RAIL IS COLLAPSED WHATEVER WAS CHOSEN.
 *
 * The stored choice is left alone — widen the window and it comes back — and the toggle goes,
 * because a control that visibly does nothing at this width is worse than no control.
 */
@media (max-width: 899.98px) {
  :root { --mf-navw: 54px; --mf-navlabel: none; --mf-navshort: inline; }
  .sessioncard, .navtoggle { display: none; }
  .sessionstub { display: grid; }
}

/* -------------------------------------------------------------------- main */

.main {
  grid-area: main;
  min-width: 0;
  padding: 22px 26px 40px;
  overflow-x: hidden;
  /* THE PAGE'S ONLY VERTICAL SCROLLER. Everything that used to move the document now moves this,
     which is what keeps the nav and the status bar in place. */
  overflow-y: auto;
  /* Reserve the scrollbar's width whether or not it is showing, so navigating from a short screen
     (Team) to a tall one (a session) does not shift the whole layout sideways by 15px. */
  scrollbar-gutter: stable;
  /* Reaching the end of this pane must not start scrolling something behind it. With the document
     no longer scrolling there is nothing behind it to move, and this keeps that true if a future
     screen ever adds one. */
  overscroll-behavior: contain;
}

/**
 * THE TOPBAR'S HAIRLINE STRENGTHENS ONCE CONTENT IS UNDER IT.
 *
 * Possible only now that `.main` is the scroller rather than the document: the chrome stays put, so
 * it can respond to the content passing beneath it. Every native application marks this moment
 * somehow, and until now there was nothing to mark — the bar scrolled away with the page.
 *
 * A SHADOW WOULD BE THE OBVIOUS WAY AND IS THE WRONG ONE HERE. `design-tokens.css` states it
 * plainly: depth is a STATE, not styling, and "NOTHING ELSE IN THE CONSOLE GETS A SHADOW" — the
 * device is the only object in the product that reads as physical, and `--e-0` is "a hairline is
 * the whole treatment". So this is a hairline that gets firmer, in the system's own language,
 * rather than an elevation borrowed from somebody else's.
 */
.topbar {
  position: relative;
  z-index: 2;
  transition: border-color 120ms ease;
}
.topbar.scrolled { border-bottom-color: var(--b-hover, #2B2B2E); }
@media (prefers-reduced-motion: reduce) {
  .topbar { transition: none; }
}

/* Scroll chaining again, for the pane most likely to be scrolled hard: reaching the end of a
   nineteen-hundred-line log should stop, not start moving the page under it. */
.logbody { overscroll-behavior: contain; }
.crumb { font-size: 10px; text-transform: uppercase; letter-spacing: .12em; color: var(--t-micro, #5A5A5A); }
.crumb button { color: inherit; }
.crumb button:hover { color: var(--t-secondary, #C4C4C4); }

.page-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin: 6px 0 18px; }
/* `display-lg` in 01: 26/30, 600, -2%. The `letter-spacing: 0` this replaces was there to stop a
   system face tracking oddly at 21px; the display face carries its own tracking above. */
.page-title { font-size: 26px; font-weight: 600; line-height: 30px; color: var(--t-primary, #F1F1F1); }
.page-sub { color: var(--t-label, #8A8A8A); }

/* The wrapping content+rail row. Never a fixed two-track grid: at 900px a grid overflows,
   this drops the rail below the content instead. */
.split { display: flex; flex-wrap: wrap; gap: var(--mf-gap, 16px); align-items: flex-start; }
.split > .content { flex: 1 1 540px; min-width: 0; display: grid; gap: var(--mf-gap, 16px); }
.split > .rail    { flex: 1 1 290px; max-width: 330px; min-width: 0; display: grid; gap: var(--mf-gap, 16px); }

@media (max-width: 900px) {
  .split > .rail { max-width: none; }
  .main { padding: 18px 14px 32px; }
}

.autogrid { display: grid; gap: var(--mf-gap, 16px); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.statgrid { display: grid; gap: var(--mf-gap, 16px); grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); }

.stat .val { font-size: 17px; color: var(--t-primary, #F1F1F1); }

/* -------------------------------------------------------------------- device card */

.device-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 14px; }
.device-meta .v { color: var(--t-body, #D4D4D4); }

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

/* -------------------------------------------------------------------- tables */

.tablewrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; min-width: 760px; }
.table th {
  padding: 10px var(--mf-rp, 13px);
  text-align: left;
  font-size: 10px; text-transform: uppercase; letter-spacing: .12em; font-weight: 400;
  color: var(--t-micro, #5A5A5A);
  border-bottom: 1px solid var(--b-row, #151517);
  white-space: nowrap;
}
.table td {
  padding: 11px var(--mf-rp, 13px);
  border-bottom: 1px solid var(--b-row, #151517);
  vertical-align: middle;
}
.table tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--hover-row); }
.table .right { text-align: right; }
.rowactions { display: inline-flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }

/* -------------------------------------------------------------------- apps */

.drop {
  display: flex; align-items: center; gap: 16px;
  padding: 20px;
  background: var(--s-card, #0D0D0F);
  border: 1px dashed var(--b-dash, #303033);
  border-radius: 3px;
  cursor: pointer;
}
.drop:hover, .drop.is-over { border-color: var(--mf-accent, #755EB8); }
.drop-icon {
  display: grid; place-items: center;
  width: 34px; height: 34px;
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: var(--r-md, 9px);
  color: var(--t-label, #8A8A8A);
  flex: 0 0 auto;
}

.buildrow {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: var(--mf-rp, 13px) var(--mf-cp, 15px);
  border-top: 1px solid var(--b-row, #151517);
}
.buildrow:first-of-type { border-top: none; }
.buildrow .idc { flex: 1 1 260px; min-width: 0; }
.buildname { color: var(--t-primary, #F1F1F1); font-weight: 600; }

/* Action status strip. */
.actionstrip { display: grid; gap: 8px; }
.actionstrip .meta { font-size: 11px; color: var(--t-caption, #6A6A6A); }

/* -------------------------------------------------------------------- cockpit */

.stage {
  background: var(--s-stage);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: 3px;
  padding: 22px;
  display: grid;
  place-items: center;
  gap: 14px;
  min-height: 260px;
}
.toolgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }

.vital { display: grid; gap: 5px; }
.vital .row { justify-content: space-between; }

.thumb { width: 56px; height: 56px; flex: 0 0 auto; background: var(--s-inset, #050506); border: 1px solid var(--b-inset, #1A1A1C); border-radius: 2px; }

/* -------------------------------------------------------------------- overlays */

.scrim {
  position: fixed; inset: 0;
  background: var(--s-scrim);
  z-index: 40;
}

.palette {
  position: fixed; top: 12vh; left: 50%;
  transform: translateX(-50%);
  width: min(560px, 100vw - 32px);
  max-height: calc(88vh - 16px);
  display: flex; flex-direction: column;
  background: var(--s-overlay, #101012);
  border: 1px solid var(--b-overlay, #2E2E31);
  border-radius: var(--r-xl, 16px);
  box-shadow: var(--e-3);
  z-index: 60;
  overflow: hidden;
}
.palette input {
  height: 48px;
  padding: 0 16px;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--b-chrome, #191919);
  color: var(--t-primary, #F1F1F1);
  font-size: 14px;
}
.palette input:focus { outline: none; }
.palette ul { overflow-y: auto; max-height: min(46vh, 340px); padding: 6px; }
.cmd {
  display: flex; align-items: center; gap: 11px;
  width: 100%;
  padding: 9px 10px;
  border-radius: var(--r-md, 9px);
  color: var(--t-body, #D4D4D4);
  font-size: 13px;
  text-align: left;
}
.cmd:hover { background: var(--hover-soft); }
.cmd .glyph {
  display: inline-grid; place-items: center;
  width: 14px; height: 14px;
  color: var(--t-label, #8A8A8A);
  flex: 0 0 auto;
}

/* The two group headings. `GO TO` and `DO`, never interleaved — see `renderPalette`. */
.cmd-group { padding: 12px 14px 5px; }
.cmd-group:first-child { padding-top: 6px; }
.cmd-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The free count or the geometry, on the row you are about to press. */
.cmd-note { color: var(--t-caption, #6A6A6A); font-size: 10.5px; flex: 0 0 auto; }
.cmd.is-sel { background: var(--mf-accent-soft, rgba(117,94,184,.16)); color: var(--t-primary); }
.palette footer {
  padding: 8px 14px;
  border-top: 1px solid var(--b-chrome, #191919);
  font-size: 10px;
  color: var(--t-navhead, #484848);
}

.dialog {
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: min(440px, 100vw - 32px);
  background: var(--s-overlay, #101012);
  border: 1px solid var(--b-overlay, #2E2E31);
  border-radius: 5px;
  box-shadow: var(--e-3);
  padding: 20px;
  z-index: 50;
}
.dialog h2 { font-size: 14px; font-weight: 600; color: var(--t-primary, #F1F1F1); }

.toasts {
  position: fixed; right: 16px; bottom: 16px;
  display: grid; gap: 8px;
  z-index: 60;
  max-width: calc(100vw - 32px);
}
.toast {
  width: 286px;
  max-width: 100%;
  padding: 10px 12px;
  background: var(--s-overlay, #101012);
  border: 1px solid var(--b-overlay, #2E2E31);
  border-left: 2px solid var(--t-label, #8A8A8A);
  border-radius: 2px;
  box-shadow: var(--e-2);
}
.toast.ok   { border-left-color: var(--ok-dot, #6FCB63); }
.toast.warn { border-left-color: var(--warn, #E8A30E); }
.toast.bad  { border-left-color: var(--bad-dot, #E0505F); }
.toast .t-title { color: var(--t-primary, #F1F1F1); font-size: 12px; }
.toast .t-body  { color: var(--t-help, #787878); font-size: 11px; }

/* ---------------------------------------------------------------------------- utilities */

/**
 * Spacing and sizing utilities, and the reason they exist rather than a `style` attribute.
 *
 * The console is served with `style-src 'self'` and no `'unsafe-inline'`, which kills the STYLE
 * ATTRIBUTE as well as <style> blocks — a `style="margin-top:20px"` silently computes to 0. Every
 * static offset therefore has to be a class here. The only exception is a value that genuinely
 * changes at runtime (a progress bar's width), which is written through CSSOM — `el.style.width =`
 * is not blocked by CSP, only the attribute is.
 */
.mt-xs { margin-top: 6px; }
.mt-sm { margin-top: 8px; }
.mt-md { margin-top: 11px; }
.mt-lg { margin-top: 14px; }
.mt-xl { margin-top: 20px; }
.mb-gap { margin-bottom: var(--mf-gap, 16px); }
.row.end { justify-content: flex-end; }
.mono-bad { color: var(--bad-mono, #F0A0A8); }
.table.wide { min-width: 880px; }
.table.narrow { min-width: 520px; }
.sc-line { margin: 5px 0 2px; }

/* A pill carrying a proper name (a build, a package) rather than a status word. The uppercase
   micro-label treatment is for states; shouting "FINANCE APP 4.18.2" misreads it as one. */
.pill.plain { text-transform: none; letter-spacing: .01em; font-size: 11.5px; }

/* ====================================================================== live view (ADR-0007)
   Everything below arrived with the live device view. The phone frame existed before it and held a
   paragraph explaining why there was no stream; it now has to hold a video, so it is sized from the
   device rather than from a sentence. */

/* ====================================================================== the device panel
   Modelled on a device-mirroring panel rather than on a picture of a phone: the screen is the
   subject, it scales to the space available, and the controls sit in a toolbar beside it instead of
   being decoration around a small rectangle.

   The panel is also the one part of this console that is NOT rebuilt on every poll — see
   `stagePanel` in console.js. The CSS below is written so that a state change is a class or a
   custom property, never a re-layout of the video. */

.devpanel {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "bar stage" "bar caption";
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 0 14px;
  background: var(--s-app);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: 3px;
  padding: 14px;
  min-height: 520px;
}

/* -------------------------------------------------------------------- toolbar */

.devbar {
  grid-area: bar;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-content: start;
  padding: 4px;
  background: var(--s-nav, #0A0A0C);
  border: 1px solid var(--b-row, #151517);
  border-radius: 3px;
  align-self: start;
}
/**
 * Rail control. 34px square, r9.
 *
 * NEVER HIDDEN AND NEVER REMOVED when unavailable — it renders dashed, dimmed and inert, with a
 * tooltip naming the missing capability. A control that disappears when the device cannot do the
 * thing leaves the reader with no way to learn that the device cannot do the thing; a control that
 * is visibly unavailable teaches them the shape of what they have.
 */
.devbtn {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md, 9px);
  color: var(--t-secondary, #C4C4C4);
  cursor: pointer;
  padding: 0;
  transition: background var(--m-fast, 160ms) var(--ease-out), border-color var(--m-fast, 160ms) var(--ease-out);
}
.devbtn:hover:not(:disabled) {
  background: var(--s-btn, #131315);
  border-color: var(--b-btn, #2B2B2E);
  color: var(--t-primary, #F1F1F1);
}
.devbtn:active:not(:disabled) { background: var(--mf-accent-soft, rgba(117, 94, 184, .16)); }
/**
 * TWO KINDS OF UNAVAILABLE, and they must not look the same.
 *
 * Stage 2 gave every disabled rail control the dashed border, which was half right and therefore
 * misleading: it made a control WAITING FOR THE STREAM look identical to one the device can never
 * honour. "Not yet" and "not ever" are the two things a person at this rail most needs to tell
 * apart, and the dash was saying "not ever" to both.
 *
 * Neither is ever "you lack permission". This console does not have that state.
 */

/* NOT YET — the channel is not open. Dimmed, solid, and it will come back on its own. */
.devbtn:disabled {
  opacity: .4;
  cursor: default;
  color: var(--t-strike, #4A4A4A);
}

/**
 * NOT EVER — the device does not declare the capability.
 *
 * Visible and inert rather than removed, which is document 04's rule and the same reasoning that
 * keeps an undeclared capability in the chip list instead of filtering it out: an absence you can
 * see is explained, and an absence you cannot see is a gap the reader has to guess at.
 *
 * Dashed for the outline of something that is not there, and STRUCK for the same reason the
 * capability chip is struck — one visual language for "declared absent", wherever it appears.
 */
.devbtn.undeclared:disabled {
  opacity: .55;
  border-style: dashed;
  border-color: var(--b-dash, #303033);
}
.devbtn.undeclared::after {
  content: '';
  position: absolute;
  left: 5px; right: 5px; top: 50%;
  height: 1px;
  background: var(--t-strike, #4A4A4A);
  transform: rotate(-32deg);
  pointer-events: none;
}
/* The strike is absolutely positioned against the button, so the button has to be a containing
   block. It was `display: grid` with no position, which put the line against the page. */
.devbtn { position: relative; }
.devbar-sep { height: 1px; margin: 5px 4px; background: var(--b-row, #151517); }

/* -------------------------------------------------------------------- the screen */

.dev-stage {
  grid-area: stage;
  display: grid;
  place-items: center;
  min-height: 0;
  min-width: 0;
  /* A subtle grid, so an all-black device screen is still distinguishable from an empty panel. */
  background:
    radial-gradient(circle at 50% 40%, var(--mf-accent-soft), transparent 60%),
    var(--s-stage);
  border-radius: 2px;
  overflow: auto;
  padding: 12px;
}
/* Positioned, because the build's tile anchors to this box — see `.dev-tile` and D15. */
.dev-fit { display: grid; place-items: center; position: relative; }

/**
 * THE COCKPIT'S FRAME IS THE SHARED COMPONENT NOW — see `frame.js` and the frame section at the end
 * of this file. What used to live here was a second, parallel implementation: a bezel drawn with
 * `box-shadow` spread, a cutout positioned as a percentage of the screen box, and side buttons that
 * only this screen knew how to draw. All of it is gone, and only the SIZING is left below, because
 * sizing is the one thing genuinely particular to a full-height stage.
 *
 * The reasoning that produced that frame survives intact and is worth keeping:
 *
 *   The panel was deliberately "a thin bezel, not a phone illustration ... no speaker grille, no
 *   camera dot, no chrome pretending to be hardware this farm does not have", and that was right
 *   while every device was a nameless 720x1280 rectangle. Devices are now configured to a specific
 *   panel and density, and a body drawn to match is not pretence — it is the rest of the same
 *   fact, and it carries information a rectangle cannot: where the thumb falls, how much of the
 *   screen a real hand covers, what the safe area actually is.
 *
 *   What survives in full: an UNPROFILED device still gets a plain, cutout-less body, because there
 *   is no handset to draw. That is the ORDINARY case on this farm, not an error state.
 *
 * THE STAGE SIZES BY HEIGHT, which is why this rule exists at all. `frame.js` derives every
 * dimension from `--f-w`, the rendered panel WIDTH — right for a card, a table row and a palette
 * result, all of which have a width to fill. A cockpit has a viewport height to fill instead, so
 * the width is computed back from it through the device's own aspect ratio. One line, and the frame
 * needs no second sizing mode.
 */
.dev-stage .mf-device {
  --f-w: calc(min(68vh, 760px) * var(--f-aspect));
  max-width: 100%;
}

/**
 * Chrome off — every profile-driven flourish disappears and the panel is a plain rounded box again.
 * One attribute, no re-layout of the video, which is the rule the whole panel is built on.
 *
 * The sheen goes to zero through `--glass-sheen` rather than through this rule, because it is a
 * value the viewer sets and not a state the console decides.
 */
.devpanel[data-chrome="off"] .mf-cutout,
.devpanel[data-chrome="off"] .mf-rails,
.devpanel[data-chrome="off"] .mf-edge,
.devpanel[data-chrome="off"] .mf-contact { display: none; }
.devpanel[data-chrome="off"] .mf-chassis {
  padding: 6px;
  background: #101012;
  box-shadow: 0 0 0 1px #2A2A2E;
  border-radius: 12px;
}
.devpanel[data-chrome="off"] .mf-glass { border-radius: 8px; }

.dev-video {
  width: 100%; height: 100%;
  /* CONTAIN, NEVER COVER. The glass box already carries the device's exact aspect ratio, so the two
     are equivalent in the ordinary case — but where they differ, `cover` crops the framebuffer, and
     hiding pixels is precisely what a device-mirroring panel must never do. */
  object-fit: contain;
  display: block;
  background: #000;
  /* The element takes keyboard focus so typing reaches the device; the default focus ring on a
     black rectangle reads as a rendering fault, so it gets the accent hairline instead. */
  outline: none;
  cursor: crosshair;
  /* Stops a drag on the device from selecting console text, and stops a touch drag from scrolling
     the page instead of swiping the phone. */
  user-select: none;
  touch-action: none;
}
/* `:focus`, NOT `:focus-visible`, and the distinction matters here more than anywhere else in the
   console. `:focus-visible` deliberately stays dark for a mouse click, which is right for a button
   and wrong for this: clicking the screen is what routes your keyboard to Android, and with no ring
   there is nothing to tell you whether the next thing you type reaches the phone or the page. */
.dev-video:focus { box-shadow: inset 0 0 0 2px var(--mf-accent, #755EB8); }

/* Says where the keystrokes are going, because the focus ring alone does not explain itself the
   first time. BELOW the phone, never on it: an earlier version floated this inside the bezel, where
   it covered Android's navigation bar and sat in the swipe-up gesture zone. The screen is the thing
   under test and must be seen as the device draws it.

   The punch-hole above is the single exception to that, and it is narrow on purpose: it is what a
   modern display genuinely contains, it takes no pointer events, and one toolbar click
   removes it. This hint is not in that category and stays outside the frame. */
.dev-kbd {
  font-size: 11px; letter-spacing: .04em; white-space: nowrap;
  color: var(--mf-accent, #755EB8);
  opacity: 0; transition: opacity .12s ease;
}
.devpanel:focus-within .dev-kbd { opacity: 1; }

/* Touch feedback, drawn locally the instant the pointer goes down.
   The round trip to the device and back through the encoder is tens of milliseconds at best, so
   without this a tap feels unanswered until the video catches up — which is the single largest
   contributor to a mirrored screen feeling sluggish, and it costs nothing to fix. The ripple is
   NOT a claim that the device received anything; it is an echo of your own input. */
.dev-taps { position: absolute; inset: 0; pointer-events: none; z-index: 2; overflow: hidden; }
.tap-ripple {
  position: absolute; width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border-radius: 50%; border: 2px solid rgba(255, 255, 255, .85);
  background: rgba(255, 255, 255, .18);
  animation: tapring .42s ease-out forwards;
}
@keyframes tapring {
  from { transform: scale(.35); opacity: .95; }
  to   { transform: scale(1.5); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .tap-ripple { animation-duration: .01ms; } }
/* The video is present from the first render and simply has nothing to show until it does. Hiding
   it rather than removing it is what keeps the element — and its stream — alive across renders. */
.devpanel[data-live="off"] .dev-video { visibility: hidden; }

.dev-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
  padding: 24px;
  text-align: center;
  /*
   * A LITERAL, AND DELIBERATELY NOT `--s-inset`.
   *
   * This element is INSIDE `.mf-glass` — it is the device's screen showing a message rather than a
   * framebuffer. A powered-off panel is near-black in a bright room and in a dark one, so it does
   * not follow the theme; using the chrome token turned the phone's screen white in light theme,
   * which is the exact boundary error stage 8 exists to find. Document 01's rule: the console is a
   * neutral desk, the DEVICE is the warm object sitting on it, and only the desk changes.
   *
   * Everything from `.mf-device` down in this file follows the same rule and for the same reason.
   */
  background: #050506;
  color: #C4C4C4;
}
/* Text on the device's screen, for the same reason: it sits on #050506 in both themes. */
.dev-overlay .micro   { color: #8A8A8A; }
.dev-overlay .help    { color: #A8A8A8; }
.dev-overlay .caption { color: #8A8A8A; }
.dev-overlay .meta,
.dev-overlay .mono    { color: #787878; }

.dev-caption {
  grid-area: caption;
  padding-top: 10px;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}
.dev-caption-text { margin: 0; }

/* The log pane explaining its own emptiness — D23. Padded like a log line, not like a card. */
.logempty { padding: 10px 12px; margin: 0; }

@media (max-width: 720px) {
  .devpanel { grid-template-columns: 1fr; grid-template-areas: "stage" "bar" "caption"; }
  .devbar { flex-direction: row; flex-wrap: wrap; justify-content: center; }
  .devbar-sep { height: 22px; width: 1px; margin: 4px 5px; }
  /* The stage's height budget shrinks; `--f-zoom` is applied inside the frame, not here. */
  .dev-stage .mf-device { --f-w: calc(min(52vh, 560px) * var(--f-aspect)); }
}

/* Screen-reader-only label. The rail is icons, and an icon is not a name — every one of them
   is `aria-hidden`, so this span is the only thing a screen reader has to read. */
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* -------------------------------------------------------------------- bring-up screen */

/* ============================================ the bring-up choreography (document 04, stage 6) ==
 *
 * SIX BEATS, EACH KEYED TO A CONFIRMED EVENT. Nothing here animates ahead of the farm: every
 * transition below is between two states the control plane has actually reported, and the two beats
 * whose interiors cannot be seen (the snapshot restore, the worker's install) BREATHE rather than
 * fill, because there is nothing to fill from.
 *
 * The element these run on is the SAME frame the cockpit shows — document 04's one continuity
 * rule. `data-mode` says which screen is currently holding it.
 */

/*
 * ---------------------------------------------------------------- the ended session (D10, D11)
 *
 * Document 04 S4: "The frame stays, dimmed and dark, at flat elevation. It is the same element it
 * has been since bring-up. Keeping it — rather than replacing the stage with an empty card — is
 * what makes the ended session read as a device you gave back instead of a page that expired."
 *
 * So the frame is not removed and not replaced. It shrinks, flattens, dims, loses its rail, and
 * moves over to make room for the numbers that are now the content of the page.
 */

/* D11 — the rail is gone, so the panel is one column instead of two. */
.devpanel[data-mode="ended"] {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "stage" "caption";
  min-height: 0;
}
.devpanel[data-mode="ended"] .devbar { display: none; }

/*
 * D10 — the stage stops claiming the viewport.
 *
 * `min(68vh, 760px)` is the live cockpit's budget and it is correct there: the device is what you
 * are working in. An ended session has nothing to work in, and at full height the four numbers
 * beside it were below the fold on any laptop.
 */
.devpanel[data-mode="ended"] .dev-stage .mf-device {
  --f-w: calc(min(38vh, 320px) * var(--f-aspect));
}
.devpanel[data-mode="ended"] .dev-stage { padding: 8px; }

/* Dimmed and dark, at flat elevation — the three words the document uses, as three declarations. */
.devpanel[data-mode="ended"] .dev-fit { opacity: .78; }
.devpanel[data-mode="ended"] .mf-chassis { box-shadow: var(--e-device-flat); }
.devpanel[data-mode="ended"] .mf-contact { opacity: 0; }

/*
 * The frame and the accounting share a row. `auto` for the stage so it takes only the width its
 * own frame needs, and the numbers take the rest — the reverse would stretch a phone.
 */
.endedwrap {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(240px, 1fr);
  gap: var(--mf-gap, 14px);
  align-items: start;
}
/* Its margin is the gap now, and a grid gap plus a card margin is two gaps. */
.endedwrap .card.ended { margin-top: 0; }

/* One column before the two would start squeezing the frame rather than the text. */
@media (max-width: 900px) {
  .endedwrap { grid-template-columns: minmax(0, 1fr); }
}

/* The bring-up screen holds the frame without the session's toolbar or its caption row. */
.devpanel[data-mode="bringup"] .devbar,
.devpanel[data-mode="bringup"] .dev-caption { display: none; }
.devpanel[data-mode="bringup"] .dev-stage {
  background: none;
  padding: 0;
  overflow: visible;
  position: relative;
}
.devpanel[data-mode="bringup"] .dev-stage .mf-device {
  --f-w: calc(min(46vh, 420px) * var(--f-aspect));
}

/* ---------------------------------------------------------------- the six beats
 *
 * DRIVEN ENTIRELY BY `data-beat`, and scoped to the bring-up screen. The cockpit shows the same
 * element with everything already arrived, so none of this applies there — the frame carries its
 * final appearance the moment `data-mode` becomes `session`, and the transition between the two is
 * beat 06's settle.
 *
 * The previous version declared transitions for beats 02, 04 and 06 and never changed the
 * properties they were watching, so three of the six did nothing at all. A transition with no state
 * change is a comment that looks like code.
 */

/*
 * BEAT 01 — the allocator claims one: the chassis resolves out of blur.
 * `opacity .06 → 1, scale .955 → 1, blur 9px → 0`. The screen stays off and the shadow stays flat:
 * a device that exists is not yet a device that is awake.
 */
.devpanel[data-mode="bringup"] .dev-fit {
  transition: filter var(--m-arrive, 700ms) var(--ease-emphatic),
              opacity var(--m-arrive, 700ms) var(--ease-emphatic),
              transform var(--m-arrive, 700ms) var(--ease-emphatic);
}
.devpanel[data-mode="bringup"][data-beat="0"] .dev-fit {
  filter: blur(9px);
  opacity: .06;
  transform: scale(.955);
}

/*
 * BEAT 02 — the clean snapshot is restored: the screen wakes.
 *
 * A centred cool glow RAMPS UP behind the glass. The design tracks the glow's radius to
 * snapshot-restore progress where that exists and says plainly what to do where it does not — "the
 * wake glow pulses instead of tracking" — and no worker reports restore progress, so it ramps in
 * and then breathes on the system loop. Both gates stop the breathing; the glow itself stays, so a
 * viewer with motion off still sees a woken screen rather than a dead one.
 */
@keyframes mf-wake-in { from { opacity: 0; } to { opacity: 1; } }
.devpanel[data-mode="bringup"] .mf-glass::before {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(62% 44% at 50% 46%, var(--mf-accent, #755EB8), transparent 70%);
  transition: opacity var(--m-arrive, 700ms) var(--ease-out);
}
.devpanel[data-mode="bringup"][data-beat="2"] .mf-glass::before,
.devpanel[data-mode="bringup"][data-beat="3"] .mf-glass::before {
  opacity: .34;
  animation: mf-wake var(--mf-pulse, 2.2s) ease-in-out infinite;
}

/*
 * BEAT 03 — the data-plane socket opens and DEPTH LANDS. The beat where the device stops being a
 * picture and becomes an object, and the only shadow change in the product.
 *
 * KEYED TO THE SOCKET, NOT THE STREAM. Depth used to land on `data-state="live"`, so a device that
 * declares no `screen-stream` never became physical at all, and one with a slow negotiation stayed
 * flat while it was already attached and driveable (migration 017: the socket is what makes a
 * session real).
 */
.devpanel[data-beat] .mf-chassis { transition: box-shadow var(--m-settle, 900ms) var(--ease-out); }
.devpanel[data-beat] .mf-contact { transition: opacity var(--m-settle, 900ms) var(--ease-out); }
.devpanel[data-beat="0"] .mf-chassis,
.devpanel[data-beat="1"] .mf-chassis,
.devpanel[data-beat="2"] .mf-chassis { box-shadow: var(--e-device-flat); }
.devpanel[data-beat="0"] .mf-contact,
.devpanel[data-beat="1"] .mf-contact,
.devpanel[data-beat="2"] .mf-contact { opacity: 0; }

/*
 * BEAT 04 — the stream negotiates: real pixels cross-fade up OVER the wake glow, and the glass
 * sheen fades in with them.
 *
 * The glow going out as the pixels come in is the cross-fade — the wake light was standing in for a
 * screen, and now there is one. Sub-states read as machine text under the step, never as motion.
 */
.devpanel[data-mode="bringup"] .mf-glass > .mf-panel,
.devpanel[data-mode="bringup"] .mf-sheen {
  transition: opacity 800ms var(--ease-out);
}
.devpanel[data-mode="bringup"]:not([data-beat="4"]):not([data-beat="5"]):not([data-beat="6"]) .mf-glass > .mf-panel,
.devpanel[data-mode="bringup"]:not([data-beat="4"]):not([data-beat="5"]):not([data-beat="6"]) .mf-sheen {
  opacity: 0;
}
.devpanel[data-mode="bringup"][data-beat="4"] .mf-glass::before,
.devpanel[data-mode="bringup"][data-beat="5"] .mf-glass::before,
.devpanel[data-mode="bringup"][data-beat="6"] .mf-glass::before {
  opacity: 0;
  animation: none;
}

/*
 * BEAT 06 — the app is open. NOTHING IS DRAWN: the app appearing IS the animation, and it arrives
 * in the framebuffer on its own. What moves is the composition, settling into the layout the
 * cockpit will hold it in.
 *
 * A scale, not a position: the frame's place on the page is the bring-up screen's grid and the
 * cockpit's is its own, and animating between two layouts would need both on screen at once. This
 * is the settle the design asks for — the device arriving at rest — and the handover to the cockpit
 * is then a mode change on an element that is already still.
 */
.devpanel[data-mode="bringup"][data-beat="6"] .dev-fit {
  transform: scale(1.015);
  transition: transform var(--m-settle, 900ms) var(--ease-out);
}

/*
 * BEAT 05 — the build waits OUTSIDE the frame and lands on confirmation.
 *
 * Document 04 travels the tile down into the screen as bytes arrive, and states its own fallback
 * for when there is no byte progress: the tile waits outside and lands when the worker confirms.
 * No worker reports install bytes, so the fallback IS the design here — and it is the honest half,
 * because a travel animation with nothing to map travel to is the bar all over again.
 */
.dev-tile {
  position: absolute;
  /*
   * D15 — ABOVE THE FRAME, MEASURED FROM THE FRAME.
   *
   * `top: 6px` measured from the STAGE, whose height is the viewport's and not the device's, so on
   * a tall screen the frame's top edge rose to meet the tile and the two overlapped. `bottom: 100%`
   * is the frame's own top edge, so the clearance below is a constant in every viewport.
   */
  bottom: 100%;
  margin-bottom: 12px;
  left: 50%;
  transform: translate(-50%, 0);
  max-width: 80%;
  padding: 6px 11px;
  border-radius: var(--r-sm, 6px);
  background: var(--s-chip, #191919);
  border: 1px solid var(--warn-line, rgba(232, 163, 14, .24));
  font-family: var(--f-mono);
  font-size: 10.5px;
  color: var(--warn, #E8A30E);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: transform var(--m-settle, 950ms) var(--ease-emphatic),
              opacity var(--m-settle, 950ms) var(--ease-out);
}
/* Queued: outside the frame, breathing on the system loop. Nothing has been confirmed yet. */
.dev-tile[data-state="active"] { animation: mf-breathe var(--mf-pulse, 2.2s) ease-in-out infinite; }
/*
 * Confirmed: it lands INSIDE the screen, once, and stops moving.
 *
 * Its own height carries it back past the edge it was waiting above; the constant is how far into
 * the glass it settles. Measured from the same edge as the waiting position, so the travel is the
 * gap the reader was looking at rather than a number tuned against one screen size.
 */
.dev-tile[data-state="done"] {
  transform: translate(-50%, calc(100% + 38px));
  border-color: var(--ok-line, rgba(111, 203, 99, .28));
  color: var(--ok-text, #7FD873);
  animation: none;
}
.dev-tile[data-state="failed"] {
  border-color: var(--bad-line, rgba(224, 80, 95, .30));
  color: var(--bad-text, #EE7C88);
  animation: none;
}

.bringup { display: flex; gap: 22px; align-items: flex-start; flex-wrap: wrap; }
.bringup-stage {
  flex: 0 0 auto;
  display: grid; justify-items: center; gap: 10px;
  background: var(--s-stage);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: 3px;
  padding: 26px 34px;
}
.bringup-steps {
  flex: 1 1 340px; min-width: 0;
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: 3px;
  padding: var(--mf-cp, 15px);
}

.steplist { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 12px; }
.step { display: flex; gap: 10px; align-items: flex-start; }
.step-mark {
  width: 16px; height: 16px; flex: 0 0 auto; margin-top: 1px;
  display: grid; place-items: center;
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: 50%;
  font-size: 10px; line-height: 1;
  color: var(--t-caption, #6A6A6A);
}
.step-label { color: var(--t-help, #787878); font-size: 13px; }
.step.done .step-mark { border-color: var(--ok-dot, #6FCB63); color: var(--ok-dot, #6FCB63); }
.step.done .step-label { color: var(--t-body, #D4D4D4); }
.step.failed .step-mark { border-color: var(--bad-dot, #E0505F); color: var(--bad-dot, #E0505F); }
.step.failed .step-label { color: var(--t-primary, #F1F1F1); }
.step.active .step-mark {
  border-color: var(--mf-accent, #755EB8);
  /* The only motion on this screen, and it marks exactly one row: the step being waited on. */
  animation: stepspin var(--mf-pulse, 2.2s) linear infinite;
  border-right-color: transparent;
}
.step.active .step-label { color: var(--t-primary, #F1F1F1); }
.step.skipped { opacity: .55; }

/*
 * D16 — THE TWO BEATS THE WORKER ANSWERS FOR BREATHE, AND THEY BREATHE AMBER.
 *
 * Document 04: "Steps 5 and 6 are different, and look different… There is nothing to fill, so these
 * beats breathe — the amber mark pulses on the 2.2s system loop and the note says exactly what is
 * being waited on. A filling bar here would be the one lie that discredits the other five."
 *
 * The ring above SPINS, and a spin is the thing this rule exists to forbid one step further along:
 * it depicts motion toward a destination on the two steps where nothing is moving and the wait is
 * longest. Amber because it is the console's colour for "not yet, and that is expected" — the same
 * value the queued build's tile and the waiting gate already use. The gap in the ring goes with the
 * spin: a breathing mark is a whole circle changing weight, not a partial one holding still.
 */
.step.confirm.active .step-mark {
  border-color: var(--warn, #E8A30E);
  border-right-color: var(--warn, #E8A30E);
  color: var(--warn, #E8A30E);
  animation: mf-breathe var(--mf-pulse, 2.2s) ease-in-out infinite;
}

@keyframes stepspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .step.active .step-mark { animation: none; border-right-color: var(--mf-accent, #755EB8); }
  /* Still amber, still whole — it simply stops pulsing. The colour carries the state on its own. */
  .step.confirm.active .step-mark { animation: none; border-right-color: var(--warn, #E8A30E); }
}

/* -------------------------------------------------------------------- launch picker */

.launchgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--mf-gap, 16px);
  margin-bottom: var(--mf-gap, 16px);
}

.picklist { display: grid; gap: 1px; max-height: 420px; overflow-y: auto; }
.pickrow {
  display: flex; align-items: center; gap: 12px; justify-content: space-between;
  width: 100%; text-align: left;
  padding: 9px 11px;
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-row, #151517);
  border-radius: 2px;
  color: inherit; font: inherit; cursor: pointer;
}
.pickrow:hover { border-color: var(--b-hover, #3A3A3D); }
/* Selection is a left bar plus a tinted ground — never colour alone, and it survives a
   high-contrast mode that drops backgrounds. */
.pickrow.picked {
  border-color: var(--mf-accent-line, rgba(117, 94, 184, .42));
  background: var(--mf-accent-soft, rgba(117, 94, 184, .16));
  box-shadow: inset 2px 0 0 var(--mf-accent, #755EB8);
}
.pick-main { display: grid; gap: 2px; min-width: 0; }
.pick-title { color: var(--t-primary, #F1F1F1); font-size: 13px; }
.pick-sub { color: var(--t-caption, #6A6A6A); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-side { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.checkline { cursor: pointer; }
.field.narrow { width: auto; min-width: 140px; }
.btn.lg { min-height: 38px; padding: 9px 18px; font-size: 13px; }

/* -------------------------------------------------------------------- logcat dock */

.logbar { margin-bottom: 8px; }
.logbar .field { flex: 1 1 180px; }
.levelchip {
  padding: 3px 8px;
  background: var(--s-btn, #131315);
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: 2px;
  color: var(--t-caption, #6A6A6A);
  font: inherit; font-size: 11px; cursor: pointer;
}
.levelchip.on {
  background: var(--mf-accent-soft, rgba(117, 94, 184, .16));
  border-color: var(--mf-accent-line, rgba(117, 94, 184, .42));
  color: var(--t-primary, #F1F1F1);
}

.logbody {
  height: 220px;
  overflow: auto;
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 2px;
  padding: 6px 8px;
  font-size: 11px;
  line-height: 1.55;
}
.logline { display: flex; gap: 8px; white-space: pre; }
.log-t { color: var(--t-micro, #5A5A5A); flex: 0 0 auto; }
.log-l { flex: 0 0 auto; width: 8px; }
.log-g { color: var(--t-label, #8A8A8A); flex: 0 0 auto; }
/* The message is the only column allowed to wrap; the rest are fixed so the eye can scan down a
   level column the way it does in `adb logcat` itself. */
.log-m { color: var(--t-body, #D4D4D4); white-space: pre-wrap; word-break: break-word; min-width: 0; }
/* Level colours from the design handoff. They repeat the letter in the `.log-l` column beside them,
   so colour is never the only carrier. */
.logline.lE .log-l, .logline.lF .log-l { color: var(--log-e); }
.logline.lW .log-l { color: var(--log-w); }
.logline.lI .log-l { color: var(--log-i); }
.logline.lD .log-l, .logline.lV .log-l { color: var(--log-d); }
.logline.lE .log-m { color: var(--log-e); }

/* -------------------------------------------------------------------- vitals + captures */

.vit { padding: 4px 0; border-bottom: 1px solid var(--b-row, #151517); }
.vit:last-of-type { border-bottom: 0; }

.shotstrip { display: flex; gap: 7px; overflow-x: auto; padding-bottom: 4px; }
.shot {
  flex: 0 0 auto;
  width: 56px; height: 100px;
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 2px;
  overflow: hidden;
  display: block;
}
.shot:hover { border-color: var(--mf-accent-line, rgba(117, 94, 184, .42)); }
.shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------------------------------------------------------------- organisation */

/* A secret a person has to select and copy. `all` so one click takes the whole key rather than a
   word of it — a partially-copied credential fails somewhere far away from here. */
.selectable { user-select: all; -webkit-user-select: all; cursor: text; }

/* The one-time key display. Wraps, because a key is longer than the card on a narrow window and a
   horizontally-scrolled secret hides its own end. */
.keyplain {
  word-break: break-all; font-size: 13px; line-height: 1.5;
  padding: 10px 12px; border-radius: 8px;
  background: var(--s-inset, rgba(255,255,255,.04));
  border: 1px solid var(--b-inset, #1A1A1C);
}

/* ---------------------------------------------------------------- element inspector */

/* The selection box, drawn in the pointer-transparent tap layer and only while inspecting. */
.insp-box {
  position: absolute;
  border: 2px solid var(--mf-accent, #755EB8);
  background: var(--mf-accent-soft);
  border-radius: 2px;
  pointer-events: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55);
}
.insp-tag {
  position: absolute; top: -19px; left: -2px;
  padding: 1px 6px; border-radius: 4px 4px 0 0;
  font-size: 10px; line-height: 16px; white-space: nowrap;
  color: var(--on-accent, #fff); background: var(--mf-accent, #755EB8);
}

/*
 * M4 — THE SCREEN OF A DEVICE WITHOUT VIDEO.
 *
 * Inside the glass, so the same literal-colour rule as `.dev-overlay` above applies: this is the
 * device's own screen, near-black whatever the theme. The two bars float over the picture rather
 * than shortening it — the picture must keep the screen's exact aspect, because a press on it is
 * mapped to a device pixel by proportion. Scoped under `.mf-glass`, the frame's own namespace, which
 * is what tells the theme guard these literals are the device and not the console's chrome.
 */
.dev-overlay.is-still { padding: 0; display: block; }
.mf-glass .still { position: absolute; inset: 0; background: #050506; }
.mf-glass .still-surface { position: absolute; inset: 0; overflow: hidden; touch-action: none; cursor: pointer; outline: none; }
.mf-glass .still-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; user-select: none; -webkit-user-drag: none; }
/* A secure app's blank picture is not shown as if it were the screen; its elements are. */
.mf-glass .still[data-blind="yes"] .still-img { opacity: .12; }
.mf-glass .still[data-inspect="on"] .still-surface { cursor: cell; }
.mf-glass .still-boxes, .mf-glass .still-taps { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
/* A size container, so a label can be sized from its own box: the box is the phone's text bounds,
   and a fixed font cut "Alphanumeric" to "Alphan…" on the OnePlus's PIN screen. */
.mf-glass .still-box { position: absolute; border: 1px solid rgba(117, 94, 184, .38); border-radius: 2px; container-type: size; }
.mf-glass .still-box.is-clickable { border-color: rgba(117, 94, 184, .75); }
.mf-glass .still-box.is-picked { border: 2px solid var(--mf-accent, #755EB8); background: var(--mf-accent-soft, rgba(117, 94, 184, .14)); }
.mf-glass .still-label {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font-size: clamp(7px, 62cqh, 14px); line-height: 1.1; color: #D6D6D6; text-align: center;
  white-space: nowrap;
}
.mf-glass .still-bar, .mf-glass .still-type {
  position: absolute; left: 0; right: 0; display: flex; align-items: center; gap: 6px;
  padding: 4px 6px; background: rgba(8, 8, 10, .82); z-index: 3;
}
.mf-glass .still-bar { top: 0; }
.mf-glass .still-type { bottom: 0; }
.mf-glass .still-status { flex: 1; min-width: 0; font-size: 10px; color: #9A9A9A; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mf-glass .still[data-state="error"] .still-status { color: #E08A8A; }
.mf-glass .still-input {
  flex: 1; min-width: 0; font-size: 11px; padding: 3px 6px;
  background: #121215; color: #E6E6E6; border: 1px solid #2A2A30; border-radius: 4px;
}
.mf-glass .still-key {
  flex: none; font-size: 11px; line-height: 16px; padding: 2px 7px; cursor: pointer;
  background: #18181C; color: #C8C8C8; border: 1px solid #2A2A30; border-radius: 4px;
}
.mf-glass .still-key:hover { background: #222228; }

/* While inspecting, the screen is something you are reading rather than pressing. */
.devpanel[data-inspect="on"] .dev-video { cursor: cell; }

.insp-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.insp-cls { font-size: 12px; color: var(--t-primary, #F1F1F1); word-break: break-all; }

.insp-attrs { display: grid; gap: 4px; margin-top: 10px; }
.insp-attr { gap: 10px; align-items: baseline; }
/* Values are arbitrary app strings and some are long; wrap rather than push the rail wide. */
.insp-attrv { font-size: 11px; text-align: right; word-break: break-all; max-width: 68%; }

.insp-sel {
  display: grid; gap: 5px;
  padding: 8px 9px; border-radius: 8px;
  background: var(--s-inset, rgba(255,255,255,.035));
  border: 1px solid var(--b-inset, #1A1A1C);
}
.insp-copy { display: flex; align-items: center; gap: 8px; }
/* `all` so one click takes the whole selector — half an xpath fails somewhere far from here. */
.insp-val {
  flex: 1; min-width: 0;
  font-size: 11px; line-height: 1.5; word-break: break-all;
  user-select: all; -webkit-user-select: all;
}

/* A reported failure's message. Pre-wrapped because a stack trace's line breaks carry meaning, and
   scrollable because a long one must not stretch the page — the same rule tables follow. */
.failtext {
  margin: 0;
  padding: 10px 12px;
  max-height: 260px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 12px;
  line-height: 1.5;
  color: var(--t-body, #D4D4D4);
  font-family: var(--f-mono);
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 6px;
}

/**
 * REAL / VIRTUAL device badge (spec §25).
 *
 * Reads as a label, not as a status: the state pill beside it already owns colour-as-meaning, and a
 * second saturated element in the same card would compete with the one thing a person scans for.
 * So this is a quiet outline, and the two kinds differ in hue only enough to be told apart at a
 * glance in a grid — REAL borrows the accent because it is the rarer, more expensive device and the
 * one whose presence changes how a result should be read.
 *
 * Letter-spaced caps rather than a coloured dot: it has to survive being one item in a dense grid,
 * where a dot is indistinguishable from the state dot two lines below it.
 */
.kindtag {
  display: inline-flex; align-items: center;
  padding: 2px 7px;
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 2px;
  font-size: 9.5px; font-weight: 600;
  letter-spacing: .08em;
  color: var(--t-label, #8A8A8A);
  background: transparent;
}
.kindtag.real {
  color: var(--accent-text, #C9BCF0);
  border-color: var(--mf-accent-line, rgba(117, 94, 184, .42));
  background: var(--mf-accent-soft, rgba(117, 94, 184, .16));
}

/**
 * Failure classification tag (spec §18).
 *
 * Quieter than a `pill`, because it sits beside one: the state pill says "failed" and owns the
 * alarm, while this says what KIND — a qualifier, not a second alarm. `.farm` is the case worth
 * marking, since it is the farm telling you not to trust a red row you would otherwise act on.
 */
.failtag {
  display: inline-flex; align-items: center;
  padding: 1px 6px;
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 2px;
  font-size: 10px; letter-spacing: .04em;
  color: var(--t-label, #8A8A8A);
}
.failtag.farm {
  color: var(--warn, #E8A30E);
  border-color: var(--warn-line, rgba(232, 163, 14, .24));
  background: var(--warn-bg, rgba(232, 163, 14, .06));
}

/* ============================================================== the device frame ==============
 *
 * SIX NESTED ELEMENTS AND NO IMAGES. The frame is additive AROUND the panel box and never drawn
 * over it: bezel is padding on the chassis, rails sit outside the chassis edge, the contact shadow
 * is a sibling below. Exactly one element overlaps the panel — the punch-hole — and that one is
 * physically accurate, because a real camera does occlude pixels and Android reserves the region.
 *
 * EVERY DIMENSION DERIVES FROM `--f-w`, the rendered panel width. The ratios `frame.js` writes are
 * unitless, so one component serves a 12px palette result and a 600px cockpit stage from one set of
 * numbers — and a corner radius that is right at one size is right at all of them.
 *
 * THE PERFORMANCE RULES, which are not stylistic. Rendering on this farm is software and frame rate
 * is the proof the session is healthy, so compositing cost near the panel is a bug:
 *
 *   - No `backdrop-filter` anywhere near the stage. Not once.
 *   - No animated `box-shadow` or `filter` while the stream is live. The two shadow states
 *     cross-fade exactly once, on attach, and never again.
 *   - `.mf-sheen` is a STATIC gradient, never a moving highlight.
 *   - `.mf-panel` gets no transform, so the video layer keeps its own compositor layer. Zoom is
 *     applied to the chassis instead.
 */

/**
 * SIZED BY HEIGHT rather than width — every frame outside the cockpit.
 *
 * Given one width, a 720×1280 body and a 1080×2340 body come out at very different heights, so a
 * row of them reads as "one of these is bigger" rather than as "these are different shapes". Fixing
 * the height instead makes them share a baseline and differ in WIDTH, which is the comparison that
 * carries the information: same height, visibly narrower, is a stubbier panel.
 */
.mf-device[data-sized="height"] { --f-w: calc(var(--f-h) * var(--f-aspect)); }

.mf-device {
  --f-w: 300px;
  --f-h: 74px;
  --f-zoom: 1;
  --f-aspect: .4615;
  --f-bezel: .02;
  --f-panel-r: .11;
  --f-chassis-r: .13;
  --f-edge-a: 1;
  --f-edge-warm: 255, 246, 235;

  /* Resolved once, so nothing below multiplies `--f-w` a second time. */
  --f-pw: calc(var(--f-w) * var(--f-zoom));
  /**
   * THE BEZEL HAS A FLOOR, and without it the small frames are wrong rather than merely subtle.
   *
   * At 34px wide an X1 Pro's `.020w` bezel computes to 0.68px, which most browsers round away —
   * so the chassis disappears, the gradient has nowhere to render, and a device thumbnail becomes
   * an unexplained black rectangle sitting on a dark card. The ratio is right at every size a
   * person actually inspects the device at; the floor is what keeps it a DEVICE at the sizes where
   * it is only an identifier.
   */
  --f-bezel-px: max(2px, calc(var(--f-pw) * var(--f-bezel)));

  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  flex: 0 0 auto;
}

.mf-chassis {
  position: relative;
  padding: var(--f-bezel-px);
  border-radius: calc(var(--f-pw) * var(--f-chassis-r));
  box-shadow: var(--e-device-flat);
  /**
   * THE ONE TRANSITION ON THIS ELEMENT, and it fires once per session.
   *
   * Depth is a STATE here, not styling: the shadow is shallow until the data plane attaches and
   * lands when it does, so the device visibly becomes physical at the moment it becomes real. That
   * is a transition between two confirmed states — the definition of legitimate motion in this
   * system — and it is the only shadow in the console that ever moves.
   */
  transition: box-shadow var(--m-settle, 900ms) var(--ease-out);
}

/**
 * The ceramic chassis fills. Three, because there are three kinds of object.
 *
 * A gradient rather than a flat fill, and this is the ONLY gradient in the console: the chrome is
 * deliberately flat everywhere else so that the device — the one thing with material, temperature
 * and depth — is where the eye lands.
 */
.mf-device[data-kind="virtual"] .mf-chassis {
  background: linear-gradient(160deg, #2A2733 0%, #1A1822 38%, #131119 66%, #1E1B26 100%);
}
.mf-device[data-kind="physical"] .mf-chassis {
  /* Warmer and darker. A real handset is a different object and reads as one. */
  background: linear-gradient(160deg, #33302B 0%, #201E1B 40%, #161512 70%, #262320 100%);
}
.mf-device[data-kind="unprofiled"] .mf-chassis {
  /* Neutral and matte. A deliberate industrial enclosure, not a failed phone — this is the
     ORDINARY case for a farm whose devices are mostly unprofiled, and it must not read as broken. */
  background: linear-gradient(160deg, #232326 0%, #1A1A1D 45%, #151517 100%);
}

/**
 * The edge highlight: 1px of warm light along the top of the chassis, as if lit from above.
 *
 * Inset 2px so it traces the chassis rather than sitting on its outer boundary, where a browser's
 * subpixel rounding would drop it on half the radii. Neutral rather than warm on the unprofiled
 * body — a warm highlight implies a material, and we do not know what that device is made of.
 */
.mf-edge {
  position: absolute;
  inset: 2px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 1px 0 rgba(var(--f-edge-warm), calc(.22 * var(--f-edge-a)));
  border: 1px solid rgba(var(--f-edge-warm), calc(.06 * var(--f-edge-a)));
}

/* Rails hang OUTSIDE the chassis edge, so they cost the device no pixels at all. */
.mf-rails { position: absolute; inset: 0; pointer-events: none; }
.mf-rail {
  position: absolute;
  width: calc(var(--f-pw) * .009);
  min-width: 1px;
  background: linear-gradient(90deg, rgba(255, 250, 244, .10), rgba(0, 0, 0, .45));
}
.mf-rail[data-side="right"] { right: calc(var(--f-pw) * -.007); border-radius: 0 2px 2px 0; }
.mf-rail[data-side="left"]  { left:  calc(var(--f-pw) * -.007); border-radius: 2px 0 0 2px; }
.mf-device[data-rail="curved"]  .mf-rail { border-radius: 999px; }
.mf-device[data-rail="squared"] .mf-rail { border-radius: 1px; }
.mf-device[data-rail="none"]    .mf-rails { display: none; }

/* The screen box. `overflow: hidden` is what makes the panel radius real rather than drawn on top. */
.mf-glass {
  position: relative;
  width: var(--f-pw);
  aspect-ratio: var(--f-aspect);
  border-radius: calc(var(--f-pw) * var(--f-panel-r));
  overflow: hidden;
  background: #000;
  /* Below the chassis in stacking order but above its background, without a transform — a
     transform here would pull the video onto the chassis's compositor layer. */
  isolation: isolate;
}

/**
 * The panel: the framebuffer, and NOTHING ELSE IN HERE.
 *
 * No transform, ever. The video keeps its own compositor layer that way, and zoom is applied to
 * `--f-zoom` on the frame instead — which resizes the box rather than scaling a decoded frame.
 */
.mf-panel {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
  border: 0;
}

/**
 * The punch-hole. The ONE element allowed over the panel, and only because it is true: a real
 * camera occludes pixels there and Android reserves the region.
 *
 * Centred and sized FROM GEOMETRY, so it cannot land on status-bar content. It never takes a
 * pointer event, and the chrome toggle removes it — so the status bar underneath is always one
 * click away. Nothing else that wants to sit on the video gets to cite this as precedent.
 */
.mf-cutout {
  --f-cut-d: .037;
  --f-cut-y: .030;
  position: absolute;
  left: 50%;
  top: calc(var(--f-pw) * var(--f-cut-y));
  width: calc(var(--f-pw) * var(--f-cut-d));
  height: calc(var(--f-pw) * var(--f-cut-d));
  transform: translateX(-50%);
  border-radius: 50%;
  background: #000;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .06);
  pointer-events: none;
}

/**
 * The glass sheen — a STATIC gradient, never a moving highlight.
 *
 * It tints real pixels by 7% at the top edge, and that is the tradeoff: it is most of what makes
 * the frame read as glass rather than as a hole cut in a card. `--glass-sheen` is a number rather
 * than a boolean so a viewer doing visual-comparison work can take it to zero without a branch.
 */
.mf-sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(202deg, rgba(255, 246, 235, 1) 0%, transparent 50%);
  mix-blend-mode: screen;
  opacity: var(--glass-sheen, .07);
  pointer-events: none;
}

/**
 * The contact shadow: an ellipse under the device, which is what makes it read as STANDING on the
 * stage rather than floating over it.
 *
 * A sibling rather than a child, because inside the chassis it would be clipped by the corner
 * radius and read as a vignette. Hidden at small sizes: a 3px blur under a 12px palette result is
 * noise, not depth.
 */
.mf-contact {
  width: calc(var(--f-pw) * .78);
  height: calc(var(--f-pw) * .055);
  margin-top: calc(var(--f-pw) * .02);
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, .55), transparent 72%);
  opacity: 0;
  transition: opacity var(--m-settle, 900ms) var(--ease-out);
  pointer-events: none;
}

/* ---------------------------------------------------------------- the four panel states
 *
 * DEPTH IS ONE OF THE VARIABLES. The shadow is shallow until the data plane attaches, so the device
 * becomes physical at the moment it becomes real — and that is the only shadow change in the whole
 * product.
 */

/* OFF — glass reflects the room. Flat elevation, no contact shadow: nothing is standing here yet. */
.mf-device[data-state="off"] .mf-glass::after,
.mf-device[data-state="ended"] .mf-glass::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(202deg, rgba(214, 222, 238, .07), transparent 55%);
  pointer-events: none;
}

/**
 * WAKING — the snapshot is restoring, and this is THE ONLY PULSE IN THE FRAME SYSTEM.
 *
 * It is allowed because it is the one beat with no observable interior: the console cannot see
 * inside a snapshot restore, so a bar that filled here would be a depiction of progress nobody
 * reported. A pulse says "this is alive and we are waiting", which is exactly what is true. It runs
 * on `--mf-pulse`, so `data-liveness="still"` and `prefers-reduced-motion` both stop it — and the
 * step's own words still say what it is waiting for.
 */
@keyframes mf-wake { 0%, 100% { opacity: .16; } 50% { opacity: .42; } }

.mf-device[data-state="waking"] .mf-glass::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(70% 50% at 50% 46%, var(--mf-accent, #755EB8), transparent 72%);
  animation: mf-wake var(--mf-pulse, 2.2s) ease-in-out infinite;
  pointer-events: none;
}

/**
 * LIVE — full elevation lands, and real pixels are the only thing in the box.
 *
 * `::after` is deliberately absent in this state. Nothing overlays a live framebuffer except the
 * punch-hole and the sheen, both of which are physically true of a phone.
 */
.mf-device[data-state="live"] .mf-chassis { box-shadow: var(--e-device); }
.mf-device[data-state="live"] .mf-contact { opacity: 1; }

/**
 * NO SIGNAL — a sentence, never a black rectangle.
 *
 * The panel is not empty because something failed; it is empty because this device DECLARES no
 * screen-stream, which is a property of the device rather than a fault. Input, logcat, install and
 * WebDriver all still work, and the caller writes that sentence into the panel — the frame only
 * makes room for it.
 */
/* Literals, like everything else inside the glass — see `.dev-overlay`. */
.mf-device[data-state="nosignal"] .mf-glass {
  background: #050506;
  border: 1px dashed #303033;
}
.mf-device[data-state="nosignal"] .mf-sheen,
.mf-device[data-state="nosignal"] .mf-cutout { display: none; }


/**
 * SMALL SIZES drop the depth entirely.
 *
 * A device drawn at 40px in a table row is an IDENTIFIER, not an object — a cast shadow and a
 * contact ellipse at that scale are three pixels of grey mush that make the row look dirty. The
 * shape and the aspect still carry every bit of the information: a stubby 720×1280 frame is
 * recognisably not a 1080×2340 one at 24px.
 */
/**
 * A SMALL FRAME NEEDS CONTRAST BETWEEN BODY AND SCREEN, or it is not a device — it is a rectangle.
 *
 * At full size the chassis and the screen separate on material: a gradient, a cast shadow, a warm
 * edge highlight, a sheen. Every one of those is either invisible or actively harmful at 24px, so
 * what is left to say "this is a phone" is the SILHOUETTE — a lighter body with a darker screen
 * inside it. Drawn in the same near-black as the panel, the two merge and the thumbnail reads as an
 * unexplained dark slab sitting on a dark card.
 *
 * So the small chassis is lifted well clear of both the card and the screen. It is the same
 * decision the full-size frame makes with a gradient, taken with the one tool that survives at this
 * scale.
 */
/*
 * A HAIRLINE, so the silhouette reads at 40px.
 *
 * The tiny scale drops the shadow, the contact ellipse and the rails — correct, they are three
 * pixels of grey that make a row look dirty. But with all of them gone the chassis was a flat fill
 * against a dark table and the thumbnails read as black rectangles: the SHAPE, which is the entire
 * reason the frame is in the row at all, was the thing that disappeared. One border restores it and
 * costs nothing.
 */
.mf-device[data-scale="tiny"] .mf-chassis {
  box-shadow: 0 0 0 1px rgba(235, 238, 245, .10);
  background: #34323C;
}
.mf-device[data-scale="tiny"][data-kind="physical"] .mf-chassis { background: #3A3630; }
.mf-device[data-scale="tiny"][data-kind="unprofiled"] .mf-chassis { background: #2C2C30; }

/* And the screen stays the darkest thing in the row, so the silhouette reads at a glance. */
.mf-device[data-scale="tiny"] .mf-glass,
.mf-device[data-scale="tiny"] .mf-panel { background: #08080B; }
.mf-device[data-scale="tiny"] .mf-contact,
.mf-device[data-scale="tiny"] .mf-sheen,
.mf-device[data-scale="tiny"] .mf-rails { display: none; }
.mf-device[data-scale="tiny"] .mf-edge { inset: 1px; }

/**
 * A frame inside a row: a card header, a picker, a palette result.
 *
 * `align-self: center` rather than the flex default, because the frame is taller than the text
 * beside it and a stretched frame would have its aspect ratio overridden by the row — which would
 * draw a shape the device is not. The whole point of putting it here is that the shape is true.
 */
.row > .mf-device,
.pickrow > .mf-device,
.cmd .glyph > .mf-device { align-self: center; }
.cmd .glyph { width: auto; }
/* No gap between the name and the id underneath it — `.stack` adds one by default. */
.stack.none { display: flex; flex-direction: column; gap: 0; }

/**
 * The dismiss control on a warning or an error toast.
 *
 * Success toasts do not have one — they auto-dismiss in four seconds, having done their whole job.
 * A toast describing a condition that is still true does not get a timer, so it needs a way out.
 */
.toast { position: relative; }
.toast-x {
  position: absolute;
  top: 6px; right: 6px;
  display: grid; place-items: center;
  width: 20px; height: 20px;
  border-radius: var(--r-sm, 6px);
  color: var(--t-caption, #6A6A6A);
}
.toast-x:hover { background: var(--s-btn, #131315); color: var(--t-primary, #F1F1F1); }
.toast .t-title, .toast .t-body { padding-right: 18px; }

/* ================================================================ the fleet ==================
 *
 * One surface, four lenses. The lens row is a VIEW SWITCH, not a filter — same data, four
 * questions — so it reads as a segmented control rather than as the chips that narrow a list.
 */
.lensrow { gap: 4px; }

.lens {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 30px;
  padding: 6px 12px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md, 9px);
  color: var(--t-label, #8A8A8A);
  font: var(--ty-strong);
  transition: color var(--m-fast, 160ms) var(--ease-out), background var(--m-fast, 160ms) var(--ease-out);
}
.lens:hover { color: var(--t-primary, #F1F1F1); background: var(--s-btn, #131315); }
.lens.on {
  color: var(--t-primary, #F1F1F1);
  background: var(--mf-accent-soft, rgba(117, 94, 184, .16));
  border-color: var(--mf-accent-line, rgba(117, 94, 184, .42));
}

/* A count only where the count IS the answer. "Waiting 2" is what somebody came for; "Capacity 5"
   would be a second, worse copy of the headline above it. */
.lens-n {
  min-width: 18px;
  padding: 1px 5px;
  border-radius: var(--r-pill, 999px);
  background: var(--s-chip, #191919);
  font-family: var(--f-mono);
  font-size: 10px;
  color: var(--t-body, #D4D4D4);
}
.lens.on .lens-n { background: var(--hover-strong); }

/**
 * The capacity table.
 *
 * Ordered free → in use → out of the pool, and there is deliberately NO SORT CONTROL: that order is
 * the answer to the page's question, and a table you have to sort before it tells you anything has
 * not told you anything.
 */
.fleet-table td { vertical-align: middle; }
.fleet-table .stack.none { gap: 2px; }
.fleet-name { font: var(--ty-display-md); font-family: var(--f-display); letter-spacing: var(--tr-display-md); color: var(--t-primary, #F1F1F1); }
.fleet-table .rowactions { white-space: nowrap; }
/*
 * D17 — THE ID, WHERE THE ID IS THE ONLY DIFFERENCE.
 *
 * Two devices of one class carry the same name, the same geometry and the same capabilities, so on
 * those rows the short id is not incidental machine text — it is the entire answer to "which one is
 * this". It stays mono and it stays small, because it IS machine text; what it stops being is
 * caption grey, which is the console's word for "you may skip this".
 *
 * Only on the rows that need it. A unique name keeps the quiet caption, so the emphasis means
 * something when it appears rather than being a style every row wears.
 */
.fleet-id.shared {
  color: var(--t-body, #D4D4D4);
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-row, #151517);
  border-radius: var(--r-sm, 6px);
  padding: 0 5px;
  font-size: 11px;
  letter-spacing: .02em;
  align-self: start;
}
/* The device thumbnail sets the row height; without this the frame stretches to fill the cell and
   its aspect ratio — the one thing it exists to show — becomes whatever the row is. */
.fleet-table .mf-device { align-self: center; }

/* The catalogue's cards. Wider than the fleet's, because a card here carries a sentence. */
.catgrid { display: grid; gap: var(--mf-gap, 16px); grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
.cat-card { gap: var(--mf-gap, 16px); }
.cat-blurb { max-width: 46ch; text-wrap: pretty; }

/*
 * The catalogue card as a PRODUCT card — document 05 §02. Text on the left, the device at full
 * card height on the right. On a page whose whole job is "choose a screen size", the screen is the
 * largest thing on the card and two classes are told apart by silhouette before a word is read.
 */
.cat-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 18px;
  align-items: start;
}
.cat-hero { display: grid; place-items: start center; }
/* The handsets' row: full width under the grid, so they read as a different kind rather than a
   fourth product to choose between. */
.phystrip .cat-blurb { max-width: 72ch; }
/* Below the two-column breakpoint the frame would squeeze the prose to a ribbon, so it steps out. */
@media (max-width: 900px) {
  .cat-body { grid-template-columns: minmax(0, 1fr); }
  .cat-hero { display: none; }
}

/* A line inside the glass, for a panel that is not streaming — see `staticFrame`'s `note`. */
.mf-note {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  margin: 0;
  padding: 8%;
  text-align: center;
  font-family: var(--f-mono);
  font-size: 10.5px;
  color: #787878;
  pointer-events: none;
}

/* Steps: the WebDriver command trace (migration 041, ADR-0029).

   A FAILED STEP IS TINTED, NOT JUST DOTTED. The pill beside it already carries the W3C code in the
   bad tone, so the row tint is redundant on its own — and that redundancy is the point on a table
   somebody scans rather than reads. A row you can find at a glance while scrolling is the whole
   value of a step list.

   Tint rather than a left border: a border moves the cells, so a table with three failures in it
   would have three rows that do not line up with the rest.

   `--bad-bg` is the existing 7% wash, defined in BOTH themes (design-tokens 185 and 388). A new
   token invented here would have had one definition and gone invisible in the other, which is the
   theme bug this console has already paid for once. */
.table.steps tbody tr.step-bad { background: var(--bad-bg, rgba(224, 80, 95, .07)); }
.table.steps tbody tr.step-bad code { color: var(--bad-text, #E0505F); }
/* Monospace numerals so durations compare down the column by eye, which is the only reason anybody
   reads this column at all. */
.table.steps .tnum { font-variant-numeric: tabular-nums; }
/* A slow step that SUCCEEDED. Amber, never red: it did not fail, and painting it as though it had
   would put the reader's attention on the wrong row. */
.table.steps .step-slow { color: var(--warn, #E8A30E); font-weight: 500; }

/* ---------------------------------------------------------------- the session recording (S5)
 *
 * The player is the browser's `<video controls>`; this only gives it a sane box. A recording is
 * portrait (a phone panel: 720x1280 or 1080x2340), so an unconstrained element is taller than the
 * viewport and pushes the artifact list, the failure buttons and everything else below the fold —
 * on the one screen whose whole job is answering a question quickly.
 *
 * `max-height` in `vh` rather than a fixed pixel box: the constraint is the person's screen, not a
 * number, and `object-fit: contain` keeps the aspect ratio whatever the device's panel was.
 */
.evidence-video {
  width: 100%;
  max-height: 55vh;
  object-fit: contain;
  border-radius: var(--r-lg, 12px);
  /* Black, not a themed surface: it is the letterbox around a portrait panel, and a light-theme
     grey there reads as a broken image rather than as the edge of the video. */
  background: #000;
}

/* The captured log is READ, not glanced at.
 *
 * `.logbody`'s 220px is sized for the live dock beside a running device, where the useful line is
 * the one that just arrived. A finished session's log is nineteen hundred lines somebody is
 * scanning, and ten at a time turns that into scrolling rather than reading. Capped in `vh` so it
 * never grows past the viewport on a tall screen. */
.logbody.tall { height: min(60vh, 640px); }

/* --------------------------------------------------------------- usage chart (ADR-0035)
   One bar per day of metered device time. Everything static lives here; only the bar's HEIGHT is
   written at runtime, which is the rule `h()` states for using `style` at all. */
.usagebars { display: flex; align-items: flex-end; gap: 3px; height: 64px; }
.usagebars > i { flex: 1 1 0; min-width: 3px; border-radius: 2px; background: var(--mf-accent, #755EB8); display: block; }

/* --------------------------------------------------------------- collapsed step runs
   A run of identical successful commands, folded into one row that opens. The trigger is a real
   button for the keyboard and for assistive tech; it just does not look like one inside a table. */
.table.steps tbody tr.step-run td { color: var(--t-caption, #6A6A6A); }
.linkish {
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
  cursor: pointer; text-align: left;
}
.linkish:hover code { color: var(--accent-text, #C9BCF0); }
.linkish:focus-visible { outline: 2px solid var(--mf-accent-line, rgba(117, 94, 184, .42)); outline-offset: 2px; }

/* ============================================================ test rows (migration 021, 2026-09-12)
 *
 * A session that ran SEVERAL tests used to be one row reading `PASSED 5/5`. Five tests ran, five
 * were counted, and not one of their names existed anywhere in this console. These are the rules
 * for the rows that now fold out of that count, and for the Tests card on the session screen.
 */

/**
 * The caret on the count. Rotates rather than swapping glyph: a character that changes identity
 * reads as the control being replaced, and at this size the rotation is the only motion the eye
 * needs to know the row opened.
 */
.caret {
  display: inline-block;
  color: var(--t-caption, #6A6A6A);
  transition: transform var(--m-fast, 160ms) var(--ease-out, cubic-bezier(.22, 1, .36, 1));
}
.caret.open { transform: rotate(90deg); }
.linkish:hover .caret { color: var(--t-primary, #F1F1F1); }

/**
 * The unfolded row, INSET rather than merely indented.
 *
 * A nested table sharing the parent's ground reads as more rows of the same table — which is
 * exactly the wrong thing to say, because these are a different kind of thing at a different level.
 * The recessed well is the console's existing vocabulary for "contents of the thing above".
 */
.table tbody tr.subrow:hover { background: none; }
/**
 * PADDING ON THE CELL, so the nested table sits INSIDE the well rather than spanning it.
 *
 * The first version set `padding: 0` and pushed the table to the cell edges. On screen that read as
 * five more rows of the parent table — the same left edge, the same full width — which is precisely
 * the thing this treatment exists to deny: these are one session's contents, not five more
 * sessions. The inset ground alone was not enough to say so; the gutter is what does it.
 */
.table tbody tr.subrow > td {
  padding: var(--mf-rp, 13px);
  background: var(--s-inset, #050506);
  border-top: 1px solid var(--b-inset, #1A1A1C);
}
.table tbody tr.subrow > td > * { margin: 0; }

/**
 * The test list itself, in both places it appears — the run screen's fold-out and the session
 * screen's Tests card. One class so the two cannot drift into two different pictures of the same
 * data, which is the whole reason a test's name is worth rendering twice.
 *
 * `min-width: 0` overrides `.table`'s 760px floor: this one lives inside a cell that is already as
 * wide as it is going to get, and a minimum would force the PARENT table to scroll horizontally
 * just because a row was opened.
 */
.table.testrows {
  min-width: 0;
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: var(--r-md, 9px);
  overflow: hidden;
}
.table.testrows td {
  padding: 9px var(--mf-rp, 13px);
  border-bottom: 1px solid var(--b-row, #151517);
  vertical-align: top;
}
.table.testrows tr:last-child td { border-bottom: none; }
/* The name, at body size rather than display size: a list of forty is a list, not forty headings. */
.table.testrows .fleet-name { font: var(--ty-strong); color: var(--t-primary, #F1F1F1); }
.table.testrows td:first-child { width: 1%; white-space: nowrap; }

/**
 * RED FOR A TEST THE SUITE REPORTED AS FAILED, and nothing else — the same reservation the run
 * screen keeps between a failure and a farm incident, and the steps table keeps between a failed
 * command and a slow one.
 */
.table.testrows tbody tr.failed td { background: var(--bad-bg, rgba(224, 80, 95, .07)); }

/**
 * The first line of the message, under the name.
 *
 * ONE LINE, CLIPPED. A stack trace in a list row would push every other test off the screen, and
 * the full text is one press away on the Failed card or through a share. Clipped with an ellipsis
 * rather than wrapped so every row in the list stays the same height and the list stays scannable.
 */
.failline {
  margin: 3px 0 0;
  color: var(--bad-text, #EE7C88);
  font-family: var(--f-mono);
  font-size: 11px;
  max-width: 68ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ==============================================================================================
   Infrastructure Operations Center — ADR-0038

   Six classes, and every one of them exists because an existing class could not say the thing:

     `.nav-alert`   the top bar's burn segment, relocated onto a nav item. Three states, and one of
                    them is "absent", which is the point — see `paintInfraAlert`.
     `.healthboard` the six lights, whose whole job is to be readable from further away than a card.
     `.gauge`       a number that must carry its own age. `.is-stale` is what stops an hour-old
                    reading looking like a current one — migration 044's trap, in CSS.
     `.trend`       a fortnight of powered hours. Bars, not a line: fourteen discrete days.
     `.inhost`      a host card, which is denser than any other card in the console.

   COLOUR NEVER CARRIES MEANING ALONE, which is the rule the whole palette rests on. Every state
   below also reads as a word, and `.is-stale` additionally desaturates and dims — so a page printed
   in greyscale still says which numbers are current.
   ============================================================================================== */

/* ------------------------------------------------------------------ the nav dot */

/**
 * A 7px dot on the Infrastructure nav item, and NOTHING when there is nothing to say.
 *
 * Absent, never green. A permanent indicator is one people stop seeing, and the entire value of
 * this dot is that its appearance means something — the same reasoning the burn segment was written
 * with before it moved here.
 */
.nav-alert {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-left: auto;
  flex: none;
  background: var(--t-label, #8A8A8A);
}
.nav-alert.warn { background: var(--warn, #E8A30E); }
.nav-alert.bad  { background: var(--bad-dot, #E0505F); }
/* The red one pulses; the amber one does not. "The farm is broken" and "the farm is costing money"
   are different errands, and only one of them should pull the eye across the room. */
.nav-alert.bad { animation: navpulse 2.4s ease-in-out infinite; }
@keyframes navpulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .nav-alert.bad { animation: none; } }
/* In the collapsed icon rail the label is gone, so the dot rides the glyph's corner instead. */
:root[data-nav="icons"] .nav-alert { position: absolute; top: 7px; right: 7px; margin-left: 0; }
:root[data-nav="icons"] .navitem { position: relative; }

/* ------------------------------------------------------------------ the health board */

.healthboard { border-left: 3px solid var(--b-row, #151517); }
.healthboard.hb-ok   { border-left-color: var(--ok-dot, #3FB950); }
.healthboard.hb-warn { border-left-color: var(--warn, #E8A30E); }
.healthboard.hb-bad  { border-left-color: var(--bad-dot, #E0505F); }
.healthboard .page-title { font-size: 19px; margin: 0; }

.hb-grid {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin-top: var(--mf-gap, 14px);
  background: var(--b-row, #151517);
  border: 1px solid var(--b-row, #151517);
  border-radius: 8px;
  overflow: hidden;
}
.hb-item { background: var(--s-card, #0E0E10); padding: 11px 13px; min-width: 0; }
.hb-item .caption { margin: 4px 0 0; }
.hb-item .micro { color: var(--t-label, #8A8A8A); }

/* ------------------------------------------------------------------ gauges */

.gaugegrid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); }

/**
 * A STALE BLOCK IS DIMMED AND DESATURATED, not hidden.
 *
 * Hiding a number is its own kind of lie — "the disk was 97% full an hour ago" is worth knowing.
 * It simply must not look like now, and a tone change alone is not enough: the bar's colour is what
 * the eye reads first, so a stale gauge loses its colour entirely and keeps its shape.
 */
.gaugegrid.is-stale, .gauge.is-stale { opacity: .62; }
.gauge.is-stale .meter > i, .gaugegrid.is-stale .meter > i { background: var(--t-label, #8A8A8A) !important; }
.gauge .val { font: var(--ty-strong); font-variant-numeric: tabular-nums; }
.gauge .micro { margin: 0 0 2px; }
.gauge .caption { font-size: 11px; }

.meter {
  display: block;
  height: 5px;
  margin-top: 6px;
  border-radius: 3px;
  background: var(--s-btn, #131315);
  overflow: hidden;
}
.meter > i { display: block; height: 100%; background: var(--accent-text, #5B8DEF); transition: width .25s ease; }
.meter > i.ok   { background: var(--ok-dot, #3FB950); }
.meter > i.warn { background: var(--warn, #E8A30E); }
.meter > i.bad  { background: var(--bad-dot, #E0505F); }

/* ------------------------------------------------------------------ host cards */

.inhost { display: grid; gap: 6px; }
.inhost .card-title { font-size: 15px; margin: 0; }

/* ------------------------------------------------------------------ the cost trend */

.trend { display: flex; align-items: flex-end; gap: 4px; height: 104px; }
.trend-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  gap: 4px;
  height: 100%;
  min-width: 0;
}
.trend-col > i {
  display: block;
  width: 100%;
  border-radius: 3px 3px 0 0;
  background: var(--accent-text, #5B8DEF);
  opacity: .78;
}
.trend-col:hover > i { opacity: 1; }
/* Today is the column somebody is actually asking about, so it is the one that is not translucent. */
.trend-col:last-child > i { opacity: 1; background: var(--warn, #E8A30E); }
.trend-col .micro { color: var(--t-label, #8A8A8A); font-variant-numeric: tabular-nums; }

/**
 * A bullet beside a sentence that WRAPS.
 *
 * `.row` is a wrapping flex container, so a long sentence next to a dot drops to the next line
 * entirely and leaves the bullet orphaned above it. Two grid columns keep the marker on the first
 * line of its own text, which is what a bullet is for.
 */
.canline {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px;
  align-items: start;
  margin: 0 0 8px;
}
.canline > .dot { margin-top: 6px; }

/** The operations-log filter row. Wraps rather than overflowing, and never pushes the table. */
.opsfilters { flex-wrap: wrap; gap: 6px; }

/**
 * A ghost button that is about to do something irreversible — Stop, on a host card.
 *
 * NOT `danger-solid`, which is reserved for the CONFIRMATION inside the dialog. The control that
 * opens a dialog only asks a question; solid red on both would make the page look like it is
 * shouting twice, and would spend the strongest signal the palette has on the safer of the two
 * clicks. A red outline is enough to make the eye stop.
 */
.btn.ghost.danger { color: var(--bad-text, #EE7C88); border-color: var(--bad-line, rgba(224, 80, 95, .30)); }
.btn.ghost.danger:hover { border-color: var(--bad-dot, #E0505F); background: var(--bad-bg, rgba(224, 80, 95, .07)); }

/* ============================================================ the cockpit workspace (ADR-0041) ==
 *
 * THE PAGE DOES NOT SCROLL; THE PANELS DO. `.main` gives up its padding and its scrollbar for this
 * one screen (`data-layout`, set by `render`), and the workspace fills it exactly: a header strip,
 * then a grid whose 1px gap on a hairline ground IS the divider between the device and the dock.
 *
 * ONE COLUMN BELOW 1000px OF USABLE WIDTH, measured by a CONTAINER query on `.ws` rather than a
 * media query on the window. Collapsing the rail widens this box without changing the window, and
 * a window query would stack the cockpit on a screen that has room for both panels.
 *
 * The floors in the narrow block are what stop the panels overlapping. Keep them.
 */
.main[data-layout="workspace"] { padding: 0; overflow: hidden; scrollbar-gutter: auto; }

.ws {
  container: ws / inline-size;
  height: 100%;
  min-height: 0;
  display: flex; flex-direction: column;
}

.ws-head {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 10px 14px;
  padding: 11px 16px;
  background: var(--s-chrome, #050506);
  border-bottom: 1px solid var(--b-row, #151517);
}
.ws-title { flex: 1 1 320px; min-width: 0; }
.ws-titleline { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.ws-h1 {
  margin: 0;
  font: 600 18px/22px var(--f-display);
  letter-spacing: var(--tr-display-md, -.015em);
  color: var(--t-primary, #F1F1F1);
}
.ws-stream {
  padding: 1px 8px;
  border: 1px solid var(--info-line, rgba(127, 160, 216, .26));
  border-radius: var(--r-pill, 999px);
  background: var(--info-bg, rgba(127, 160, 216, .07));
  color: var(--info, #7FA0D8);
  font: 400 11px/17px var(--f-mono);
  font-variant-numeric: tabular-nums;
}
.ws-ident {
  margin: 2px 0 0;
  font: 400 11.5px/17px var(--f-mono);
  color: var(--t-caption, #6A6A6A);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ws-lease {
  display: flex; flex-direction: column; gap: 3px;
  padding: 5px 11px;
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: var(--r-md, 9px);
}
.ws-lease .micro { font-size: 10px; letter-spacing: var(--tr-label, .1em); color: var(--t-caption, #6A6A6A); }
.ws-lease-v { font: 600 13px/16px var(--f-mono); color: var(--t-primary, #F1F1F1); }

.ws-grid {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  gap: 1px;
  background: var(--b-row, #151517);
  grid-template-columns: minmax(320px, .85fr) minmax(360px, 1.15fr);
  overflow: hidden;
}

/* ---------------------------------------------------------------- left: the device */

.ws-device {
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  overflow: auto;
  background: var(--s-stage, #060607);
}
.ws-device > .card { margin: 12px 14px 0; }
.ws .devpanel {
  flex: 1 1 auto;
  min-height: 0;
  padding: 18px 14px;
  background: var(--s-stage, #060607);
  border: 0;
  border-radius: 0;
  /* The hardware column sits BESIDE the device, on the side away from the dock. */
  grid-template-columns: auto minmax(0, 1fr);
}
.ws .dev-stage { background: none; padding: 0; overflow: hidden; }
/* Sized by height, clamped, so the phone survives any stage and never outgrows a laptop panel. */
.ws .dev-stage .mf-device { --f-w: calc(clamp(300px, calc(100dvh - 250px), 430px) * var(--f-aspect)); }
.ws .devbar {
  align-self: center;
  gap: 5px;
  max-height: 100%;
  overflow-y: auto;
  padding: 0;
  background: transparent;
  border: 0;
}
.ws .devbtn { border-color: var(--b-field, #242427); background: var(--s-card, #0D0D0F); }
.ws .devbtn:hover:not(:disabled) { border-color: var(--b-hover, #3A3A3D); background: var(--s-btn, #131315); }

.ws-appstrip {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 8px 10px;
  padding: 10px 14px;
  background: var(--s-nav, #0A0A0C);
  border-top: 1px solid var(--b-row, #151517);
}
.ws-appchip {
  padding: 2px 9px;
  background: var(--s-chip, #191919);
  border: 1px solid var(--b-field, #242427);
  border-radius: var(--r-pill, 999px);
  font: 400 11.5px/17px var(--f-mono);
  color: var(--t-secondary, #C4C4C4);
}
.ws-appstrip .caption { font-size: 11.5px; color: var(--t-help, #787878); }

/* ---------------------------------------------------------------- right: the dock */

.ws-dock {
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  background: var(--s-app, #08080A);
}
.ws-tabs {
  flex: 0 0 auto;
  display: flex; gap: 2px;
  padding: 6px 8px;
  background: var(--s-nav, #0A0A0C);
  border-bottom: 1px solid var(--b-row, #151517);
  overflow-x: auto;
}
.ws-tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 10px;
  border-radius: var(--r-md, 9px);
  color: var(--t-label, #8A8A8A);
  font-size: 12.5px; font-weight: 500;
  white-space: nowrap;
}
.ws-tab:hover { color: var(--t-primary, #F1F1F1); background: var(--hover-soft); }
.ws-tab.is-on { background: var(--s-btn, #131315); color: var(--t-primary, #F1F1F1); }
.ws-count {
  padding: 0 6px;
  border-radius: var(--r-pill, 999px);
  background: var(--s-btn, #131315);
  color: var(--t-caption, #6A6A6A);
  font: 400 10.5px/16px var(--f-mono);
}
.ws-tab.is-on .ws-count { background: var(--mf-accent-soft, rgba(117, 94, 184, .16)); color: var(--accent-text, #C9BCF0); }

.ws-panel { flex: 1 1 auto; min-height: 0; overflow: auto; }
/* A card inside a panel is a section of the panel, not a floating box inside a box. */
.ws-panel > .card { margin: 0; border: 0; border-radius: 0; background: transparent; border-bottom: 1px solid var(--b-row, #151517); }
.ws-panel > .card:last-child { border-bottom: 0; }
.ws-note { padding: var(--mf-cp, 15px); }

/* The live log fills its panel: bar, rows, footer — and only the rows scroll. */
.ws-panel-logs:has(> .logdock) { display: flex; flex-direction: column; overflow: hidden; }
.logdock { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.logdock .logbar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 6px 8px;
  margin: 0;
  padding: 8px 12px;
  background: var(--s-nav, #0A0A0C);
  border-bottom: 1px solid var(--b-row, #151517);
}
.logdock .logfilter {
  flex: 1 1 160px;
  min-width: 0;
  background: var(--s-inset, #050506);
  border-color: var(--b-field, #242427);
  border-radius: 8px;
  font: 400 11.5px/17px var(--f-mono);
}
.logdock .levelchip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px;
  background: transparent;
  border-color: var(--b-card, #1F1F22);
  border-radius: var(--r-pill, 999px);
  color: var(--t-micro, #5A5A5A);
  font-size: 11.5px;
}
.logdock .levelchip .lv-n { font: 400 10.5px/1 var(--f-mono); }
.logdock .levelchip.lvE.on { border-color: var(--bad-line); background: var(--bad-bg); color: var(--bad-text); }
.logdock .levelchip.lvW.on { border-color: var(--warn-line); background: var(--warn-bg); color: var(--warn); }
.logdock .levelchip.lvI.on { border-color: var(--info-line); background: var(--info-bg); color: var(--info); }
.logdock .levelchip.lvD.on { border-color: var(--b-field); background: var(--s-btn); color: var(--log-d); }
.logdock .levelchip.scope.on { border-color: var(--mf-accent-line); background: var(--mf-accent-soft); color: var(--t-primary); }
.followchip {
  padding: 3px 10px;
  background: var(--s-btn, #131315);
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: var(--r-pill, 999px);
  color: var(--t-secondary, #C4C4C4);
  font-size: 11.5px; font-weight: 500;
}
.followchip.on { border-color: var(--ok-line); background: var(--ok-bg); color: var(--ok-text); }
.logdock .logbody {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
  padding: 6px 0;
  background: var(--s-inset, #050506);
  border: 0;
  border-radius: 0;
}
.logfoot {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 6px 8px;
  padding: 8px 12px;
  background: var(--s-nav, #0A0A0C);
  border-top: 1px solid var(--b-row, #151517);
}
.logfoot .caption { font-size: 11.5px; color: var(--t-help, #787878); }

/* Rows as four columns, so the eye can run down the level letter the way it does in adb logcat. */
.ws-panel .logline {
  display: grid;
  grid-template-columns: 88px 18px minmax(0, 132px) minmax(0, 1fr);
  gap: 10px;
  padding: 2px 14px;
  white-space: normal;
  font: 400 11px/17px var(--f-mono);
  font-variant-numeric: tabular-nums;
}
.ws-panel .logline:hover { background: var(--hover-soft); }
.ws-panel .logline.lE, .ws-panel .logline.lF { background: var(--bad-bg); }
.ws-panel .log-l { width: auto; font-weight: 700; }
.ws-panel .log-g { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-panel .logline.lW .log-m { color: var(--log-w); }
.ws-panel .logline.lI .log-m { color: var(--log-i); }
.ws-panel .logline.lD .log-m, .ws-panel .logline.lV .log-m { color: var(--log-d); }

@container ws (max-width: 999.98px) {
  /**
   * ROWS SIZED BY THEIR CONTENT, NEVER SQUEEZED. The first version put a 440px floor on the device
   * panel inside a stretched row, and the row honoured the floor instead of the content — so on the
   * farm at 1000px the fifteen-button hardware column and the phone drew straight over the dock's
   * tabs. The device row is `max-content`; the dock gets a bounded row so its log still scrolls
   * inside itself rather than growing the page by six hundred lines.
   */
  .ws-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: max-content minmax(420px, 70vh);
    overflow: auto;
  }
  .ws-device { min-height: 440px; overflow: visible; }
  /* The hardware column becomes a wrapping row above the phone: a column that tall is the panel. */
  .ws .devpanel {
    flex: 0 0 auto;
    min-height: 376px;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
    grid-template-areas: "bar" "stage" "caption";
    gap: 10px 0;
  }
  .ws .devbar { flex-direction: row; flex-wrap: wrap; justify-content: center; max-height: none; overflow: visible; }
  .ws .devbar-sep { width: 1px; height: auto; margin: 5px 4px; }
  .ws .dev-stage .mf-device { --f-w: calc(340px * var(--f-aspect)); }
  .ws-dock { min-height: 0; }
}

/* ============================================================ Apps (ADR-0041) ===================== */

/* A list screen reads best at a measure, not edge to edge on an ultrawide. Run detail shares it. */
.page-narrow { max-width: 1180px; margin: 0 auto; display: grid; gap: 16px; }
.page-narrow > div > .page-head { margin-bottom: 0; }
.page-narrow .page-sub { max-width: 70ch; font-size: 13.5px; }

.holdbanner {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 8px 10px;
  padding: 10px 14px;
  background: var(--ok-bg, rgba(111, 203, 99, .10));
  border: 1px solid var(--ok-line, rgba(111, 203, 99, .28));
  border-radius: var(--r-lg, 12px);
}
.holdbanner.idle { background: var(--s-card, #0D0D0F); border-color: var(--b-card, #1F1F22); }

.apps-grid {
  display: grid;
  gap: 16px;
  align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);
}
@media (max-width: 899.98px) { .apps-grid { grid-template-columns: minmax(0, 1fr); } }

.drop {
  padding: 16px;
  background: var(--s-nav, #0A0A0C);
  border-color: var(--b-dash, #303033);
  border-radius: var(--r-lg, 12px);
}
.drop:hover, .drop.is-over { background: var(--s-card, #0D0D0F); }
.drop-icon { width: 38px; height: 38px; }

.appsrow {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px 16px;
}
.appsrow:hover { background: var(--hover-row); }
.build-id { min-width: 0; }
.build-meta { font: 400 11.5px/17px var(--f-mono); color: var(--t-caption, #6A6A6A); overflow-wrap: anywhere; }
.build-status { display: grid; gap: 4px; min-width: 0; }
.build-error { font: 400 11px/17px var(--f-mono); color: var(--bad-mono, #F0A0A8); overflow-wrap: anywhere; background: none; padding: 0; }
.build-details { grid-column: 1 / -1; }
@media (max-width: 719.98px) {
  .appsrow { grid-template-columns: minmax(0, 1fr); }
  .appsrow .rowactions { justify-content: flex-start; }
}

/* ============================================================ Run detail (ADR-0041) ================ */

/* The sub-line breaks under the title and the verdict pill, whatever order the head builds them in. */
.page-head > .run-sub {
  order: 5;
  flex-basis: 100%;
  margin: 0;
  font: 400 11.5px/17px var(--f-mono);
  color: var(--t-caption, #6A6A6A);
}

.factgrid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.fact {
  display: grid; gap: 4px; align-content: start;
  padding: 13px 14px;
  background: var(--s-card, #0D0D0F);
  border: 1px solid var(--b-card, #1F1F22);
  border-radius: var(--r-lg, 12px);
}
.fact .micro { font-size: 10px; letter-spacing: var(--tr-label, .1em); }
.fact-v {
  margin: 0;
  font: 600 26px/30px var(--f-display);
  letter-spacing: var(--tr-display-lg, -.02em);
  font-variant-numeric: tabular-nums;
  color: var(--t-primary, #F1F1F1);
}
.fact.bad .fact-v { color: var(--bad-text, #EE7C88); }
.fact.warn .fact-v { color: var(--warn, #E8A30E); }
.fact .caption { font-size: 11.5px; }

/* Side by side on a desktop, because that is how the judgement is made; stacked only when narrow. */
.run-judge { display: grid; gap: 16px; align-items: start; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.card.judge-fail { border-color: var(--bad-line, rgba(224, 80, 95, .30)); }
.card.judge-farm { border-color: var(--warn-line, rgba(232, 163, 14, .24)); }
.run-judge .failtext {
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-inset, #1A1A1C);
  border-radius: 7px;
  font: 400 11px/17px var(--f-mono);
  color: var(--bad-mono, #F0A0A8);
}
.failitem + .failitem { padding-top: 12px; border-top: 1px solid var(--b-row, #151517); }

.flakerow { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 6px 14px; }
.flakename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--t-secondary, #C4C4C4); }
.flakestrip { display: inline-flex; gap: 3px; }
.flakecell { width: 10px; height: 16px; border-radius: 2px; background: var(--ok-line, rgba(111, 203, 99, .28)); }
.flakecell.fail { background: var(--bad-dot, #E0505F); }
/* The run being viewed, marked by an outline rather than a colour — colour already means outcome. */
.flakecell.now { outline: 1px solid var(--t-secondary, #C4C4C4); outline-offset: 1px; }
.flakeverdict { font-size: 12px; }
@media (max-width: 719.98px) { .flakerow { grid-template-columns: minmax(0, 1fr); } }

.table.runsessions { min-width: 720px; }
.testcell { gap: 3px; }
.testline { font-size: 12.5px; }

/* ============================================================ overlays (ADR-0041) ================= */

.dialog { border-radius: var(--r-xl, 16px); }
.dialog[data-kind="share"] { width: min(560px, 100vw - 32px); }
.sharelink .copyrow code { color: var(--accent-text, #C9BCF0); }

/* A real checkbox, visually replaced — it stays the thing that is focused, clicked and announced. */
.checkrow { position: relative; display: flex; align-items: flex-start; gap: 10px; padding: 4px 0; cursor: pointer; }
.checkrow input { position: absolute; left: 0; top: 6px; width: 15px; height: 15px; margin: 0; opacity: 0; }
.checkmark {
  position: relative;
  flex: 0 0 auto;
  width: 15px; height: 15px;
  margin-top: 2px;
  background: var(--s-inset, #050506);
  border: 1px solid var(--b-btn, #2B2B2E);
  border-radius: var(--r-xs, 4px);
}
.checkrow input:checked + .checkmark { background: var(--mf-accent, #755EB8); border-color: var(--mf-accent, #755EB8); }
.checkrow input:checked + .checkmark::after {
  content: '';
  position: absolute;
  left: 4px; top: 1px;
  width: 4px; height: 8px;
  border: solid var(--on-accent, #fff);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.checkrow input:focus-visible + .checkmark { outline: 2px solid var(--b-focus, #8A74D6); outline-offset: 2px; }


/* ---------------------------------------------------------------------------- AI testing (ADR-0043)
 * Only what the screen needs that no existing component gives it. The mode switch is a `.lens` row
 * and the budget is a `.meter`, so it reads as the same console rather than a bolted-on product. */
.ai-prompt {
  width: 100%;
  min-height: 108px;
  resize: vertical;
  margin-top: 6px;
  font: var(--ty-body);
  line-height: 1.5;
}
.ai-metric { font: var(--ty-display-md); letter-spacing: var(--tr-display-md); color: var(--t-primary, #F1F1F1); }
.ai-row-prompt { color: var(--t-body, #D4D4D4); overflow-wrap: anywhere; }
.ai-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
/* What a route repair changed (ADR-0046): one line a step, the kind in the left rule. */
.ai-route-diff { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.ai-route-diff li { padding: 4px 8px; border-left: 3px solid var(--warn-line); background: var(--warn-bg); border-radius: 4px; }
.ai-route-diff li.ai-route-added { border-left-color: var(--ok-line); background: var(--ok-bg); }
.ai-route-diff li.ai-route-removed { border-left-color: var(--bad-line); background: var(--bad-bg); }
.ai-step-btn {
  display: flex; gap: 10px; align-items: flex-start; width: 100%; text-align: left;
  padding: 8px 10px; border: 1px solid transparent; border-radius: var(--r-md, 9px);
  background: transparent; color: var(--t-body, #D4D4D4); cursor: pointer;
}
.ai-step-btn:hover { background: var(--s-btn, #131315); }
.ai-step.on .ai-step-btn {
  background: var(--mf-accent-soft, rgba(117, 94, 184, .16));
  border-color: var(--mf-accent-line, rgba(117, 94, 184, .42));
}
.ai-step-n {
  flex: none; min-width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill, 999px); background: var(--s-chip, #191919);
  font: var(--ty-machine); color: var(--t-label, #8A8A8A);
}
.ai-step.miss .ai-step-n { color: var(--warn, #E8A30E); }
.ai-thought { white-space: pre-wrap; overflow-wrap: anywhere; }
/* The whole phone, always: capped to the viewport so a tall 1080x2340 screen never needs scrolling
   inside the panel it is supposed to be glanced at. */
.ai-shot {
  display: block; width: auto; max-width: 100%; height: auto; max-height: calc(100vh - 150px); margin: 0 auto;
  object-fit: contain; border-radius: var(--r-lg, 12px); border: 1px solid var(--b-card, #1F1F22);
}
/* Pinned while the steps scroll: choosing a step is for seeing its screen (2026-09-26 — it scrolled
   away, so a step picked at the bottom of a long run changed a picture nobody could see). `.main` is
   the scroller, below the top bar, so the offset is only the page's own breathing room. */
.split > .rail.ai-rail { position: sticky; top: 16px; }
@media (max-width: 900px) {
  .split > .rail.ai-rail { position: static; }
}
.ai-task { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--t-primary, #F1F1F1); }
.ai-stop { color: var(--t-primary, #F1F1F1); }
.ai-raw > summary { cursor: pointer; }
.ai-raw > p { margin-top: 6px; white-space: pre-wrap; overflow-wrap: anywhere; }
.ai-step-err { color: var(--warn, #E8A30E); overflow-wrap: anywhere; }
.ai-row-why { color: var(--warn, #E8A30E); }
.backrow { margin: 0 0 6px; }
/* ADR-0044: the three things a run needs, each with a dot, above a Start that waits for all three. */
.ai-checks { gap: 16px; }
.ai-check { display: inline-flex; align-items: center; gap: 6px; }
.ai-check .micro { color: var(--t-label, #8A8A8A); }
.ai-check.bad .caption { color: var(--warn, #E8A30E); }
.ai-gate { border-color: var(--warn-line, rgba(232, 163, 14, .35)); }

/* Saved-test history: one dot per recent run, newest first. The colours are the pills'. */
.ai-dot {
  width: 10px; height: 10px; padding: 0; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--t-label, #8A8A8A);
}
.ai-dot.passed { background: var(--ok-dot, #6FCB63); }
.ai-dot.failed { background: var(--bad-dot, #E0505F); }
.ai-dot.error  { background: var(--warn, #E8A30E); }
.ai-dot.queued, .ai-dot.running { background: var(--mf-accent, #755EB8); }

/* C8: an AI diagnosis sits under the failure it explains. */
.ai-diag { margin-top: 6px; }
.ai-diag .glyph { color: var(--mf-accent, #755EB8); }
.ai-evidence { margin: 0; padding-left: 16px; font: var(--ty-log); color: var(--t-body, #D4D4D4); overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------- AI testing, 2026-09-27
 * The list first, one line per run; a run page whose title is its task; a screen that shows where the
 * agent touched it; the recording and log beside the run. See the proposals in docs/AI_PRODUCT_LINE.md. */
.ai-toolbar { gap: 8px 12px; align-items: center; margin-bottom: 10px; }
.ai-search { width: 220px; max-width: 100%; }
.ai-rows { display: flex; flex-direction: column; }
/* One saved secret: its {{NAME}}, when it was set, and what can be done to it — never its value. */
.ai-secret-row { gap: 12px; padding: 10px 0; border-top: 1px solid var(--b-row, #19191B); }
.ai-secret-row:first-child { border-top: 0; }
/* One run, one line — about 40px where a row was 90 (proposal 7). A link, so it opens in a new tab. */
.airow {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr) minmax(0, 150px) 118px 86px 64px;
  align-items: center; gap: 12px;
  min-height: 40px; padding: 6px 8px;
  border-top: 1px solid var(--b-row, #19191B);
  color: inherit; text-decoration: none;
}
.airow:first-child { border-top: 0; }
.airow:hover { background: var(--s-btn, #131315); }
.airow:focus-visible { outline: 2px solid var(--b-focus, #8A74D6); outline-offset: -2px; }
.airow.live { background: var(--mf-accent-soft, rgba(117, 94, 184, .08)); }
.airow-main { min-width: 0; display: flex; flex-direction: column; }
.airow-task, .airow-why, .airow-cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.airow-task { color: var(--t-primary, #F1F1F1); }
.airow-why { color: var(--warn, #E8A30E); font-size: 11.5px; }
.airow-cell { font-size: 12px; color: var(--t-caption, #8F8F8F); }
.airow-when { text-align: right; }
@media (max-width: 760px) {
  .airow { grid-template-columns: 96px minmax(0, 1fr) 56px; }
  .airow-cell:not(.airow-when) { display: none; }
}

/* The run page's head: the task is the title, clamped to two lines; the whole task is under Details. */
.ai-head .page-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  overflow-wrap: anywhere;
}
.ai-head .page-head { align-items: flex-start; }
.ai-facts { margin: 4px 0 6px; gap: 8px; }
.ai-details > summary { cursor: pointer; list-style: none; }
.ai-details > summary::-webkit-details-marker { display: none; }
.ai-details[open] > summary { margin-bottom: 4px; }
.ai-about > summary { cursor: pointer; }
.ai-step-body { padding: 2px 10px 10px 42px; display: grid; gap: 6px; }

/* More (proposal 15): a menu of what else a run offers. */
.menu-wrap { position: relative; display: inline-flex; }
.menu {
  position: absolute; top: calc(100% + 4px); right: 0; z-index: 20;
  min-width: 220px; padding: 4px;
  background: var(--s-overlay, #101012); border: 1px solid var(--b-overlay, #2E2E31);
  border-radius: var(--r-md, 9px); box-shadow: var(--e-2);
  display: flex; flex-direction: column;
}
.menu-item {
  display: block; width: 100%; padding: 7px 10px; text-align: left;
  background: transparent; border: 0; border-radius: var(--r-sm, 6px);
  color: var(--t-body, #D4D4D4); font: inherit; font-size: 12.5px; text-decoration: none; cursor: pointer;
}
.menu-item:hover, .menu-item:focus-visible { background: var(--s-btn, #131315); color: var(--t-primary, #F1F1F1); outline: none; }

/* Where the agent touched the screen (proposal 13). The wrapper shrinks to the image, so a mark's
   percentages are percentages of the picture and not of the card around it. */
.ai-shot-frame { text-align: center; }
.ai-shot-wrap { position: relative; display: inline-block; max-width: 100%; vertical-align: top; }
.ai-shot-wrap > .ai-shot { margin: 0; }
.ai-mark-box {
  position: absolute; pointer-events: none;
  border: 2px solid var(--mf-accent, #755EB8); border-radius: 4px;
  background: var(--mf-accent-soft, rgba(117, 94, 184, .16));
}
.ai-mark-tap {
  position: absolute; pointer-events: none; width: 18px; height: 18px; margin: -9px 0 0 -9px;
  border-radius: 50%; border: 2px solid var(--on-accent, #fff); background: var(--mf-accent, #755EB8);
}

/* Recording & log beside the run (proposal 14). Under the scrim, so Share still opens over it. */
:root { --ai-panel-w: clamp(380px, 36vw, 560px); }
.ai-panel {
  position: fixed; top: 50px; right: 0; bottom: 0; z-index: 30;
  width: min(var(--ai-panel-w), 100vw);
  display: flex; flex-direction: column;
  background: var(--s-overlay, #101012); border-left: 1px solid var(--b-overlay, #2E2E31);
  box-shadow: var(--e-2);
}
.ai-panel[hidden] { display: none; }
/* BESIDE, NOT OVER: on a screen wide enough for both, the page gives up the panel's width, so the
   run's title, steps and screen stay readable next to its recording. Narrower, the panel covers the
   page as before — two columns of 300px help nobody. */
@media (min-width: 1100px) {
  .shell.ai-panel-open .main { padding-right: calc(var(--ai-panel-w) + 26px); }
}
.ai-panel-head { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--b-row, #19191B); }
.ai-panel-body { flex: 1 1 auto; overflow: auto; padding: 14px; }
.ai-panel-body > .card { margin: 0; }
.ai-panel-foot { padding: 8px 14px; border-top: 1px solid var(--b-row, #19191B); }

.toast .t-act { margin-top: 8px; }
