/* OxOnOS appliance UI — paper-light layout and components.
   No framework, no build step, no bundler (CONTRACTS §9).

   Structure taken from OxOnOS-UI-Template.html (founder-approved, 1 Sep 2026);
   every colour comes from css/tokens.css, never from the template's inline
   values. See the header of tokens.css for the four the tokens overrode. */

* { box-sizing: border-box; margin: 0; padding: 0; }
html { height: 100%; }
body {
  height: 100%;
  background: var(--paper);
  color: var(--ink);
  font: var(--w-regular) var(--t-body)/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--ink); color: var(--paper); }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
a { color: inherit; }
input, textarea { font: inherit; color: inherit; }

/* Focus is a black ring on paper — 18.6:1, visible on every surface we have. */
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: var(--r-sm); }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 99;
  background: var(--ink); color: var(--paper); padding: 10px 18px;
  border-radius: 0 0 var(--r-md) 0; font-weight: var(--w-medium);
}
.skip-link:focus { left: 0; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ============================== shell ============================== */
.shell { display: flex; min-height: 100vh; }
.rail {
  width: var(--rail-w); flex: 0 0 auto;
  border-right: 1px solid var(--alu);
  display: flex; flex-direction: column;
  padding: 28px 20px 20px;
  position: sticky; top: 0; height: 100vh;
}
.content { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* ---------- rail / brand ----------
   ⛔ BRAND RULES 1, 2 AND 4. The template drew the rail emblem as two plain
   stacked circles in inline SVG. That is a redraw: it loses the crossing loop
   and the lower-right O-derived n-notch, which rule 1 forbids altering and
   rule 2 forbids replacing with a generic infinity form. The approved master
   PNG is placed instead, untouched — it is a LOCK-UP and already carries the
   OxOnOS wordmark beside the vertical emblem, so no second wordmark is set in
   type (the same mistake the boot screen documents).
   Rule 6: 140px minimum for a full lock-up — this is 148px. */
.brand { padding: 0 8px; }
.brand img { display: block; width: 148px; height: auto; }
.brand-tag {
  padding: 12px 8px 0;
  font-size: var(--t-caption); font-weight: var(--w-medium);
  letter-spacing: normal;
  color: var(--ink-40); text-transform: none;
}

.nav { margin-top: 30px; display: flex; flex-direction: column; gap: 2px; }
.nav a {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 10px; border-radius: var(--r-md);
  text-decoration: none; font-size: var(--t-body); color: var(--ink-60);
  border: 1px solid transparent;
}
.nav a:hover { background: var(--paper-deep); color: var(--ink); }
.nav a[aria-current="page"] {
  background: var(--paper-raise); color: var(--ink); font-weight: var(--w-medium);
  border-color: var(--alu-soft);
}
.nav a svg { flex: 0 0 auto; opacity: .75; }
.nav a[aria-current="page"] svg { opacity: 1; }

/* ⛔ There is no .netcard. The template pinned an "Offline" card to the foot of
   the rail; it is gone, along with the topbar chip that said the same thing.
   See the ruling in js/ui.js — an appliance designed to work unplugged must
   not spend every screen apologising for being unplugged. */
.rail-foot { margin-top: auto; display: flex; flex-direction: column; gap: 12px; }
.rail-meta { padding: 0 8px; font-size: var(--t-caption); color: var(--ink-40); }

/* ---------- dots, tags, pills ---------- */
.dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; display: inline-block; }
.dot.ok   { background: var(--local); }
.dot.warn { background: var(--amber); }
.dot.bad  { background: var(--red); }
.dot.idle { background: var(--alu); }

/* A tag is colour + WORD, always. Colour is never the only carrier. */
.tag {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; white-space: nowrap;
}
.tag.ok   { color: var(--local); }
.tag.warn { color: var(--amber); }
.tag.bad  { color: var(--red); }
.tag.mute { color: var(--ink-40); }

.pill {
  display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid var(--alu-soft); border-radius: var(--r-pill);
  padding: 5px 13px; font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  background: var(--paper-raise); white-space: nowrap;
}
.pill.local   { color: var(--local); border-color: color-mix(in srgb, var(--ok) 30%, transparent); background: var(--local-tint); }
.pill.warn    { color: var(--amber); border-color: color-mix(in srgb, var(--warn) 35%, transparent); background: var(--amber-tint); }
.pill.unknown { color: var(--ink-60); }

/* ---------- topbar ---------- */
.topbar {
  display: flex; align-items: center; gap: 14px;
  padding: 18px 40px; border-bottom: 1px solid var(--alu);
  position: sticky; top: 0; background: var(--paper); z-index: 10;
}
.crumb { font-size: var(--t-small); color: var(--ink-40); }
.crumb b { color: var(--ink); font-weight: var(--w-medium); }

/* ---------- which box this is (founder, 2026-10-02) ----------
   Ink on paper, the brand's own two colours, so it reads as part of the
   product and not as a warning. It never shrinks: the crumb beside it is the
   elastic member (see the overflow note further down), and on a phone only
   the short id gives way. */
.boxbadge {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: var(--ink); color: var(--paper);
  font-size: var(--t-caption); font-weight: var(--w-bold); letter-spacing: .02em; white-space: nowrap;
}
.boxbadge[hidden] { display: none; }
.boxbadge .bb-id {
  font-size: var(--t-caption); font-weight: var(--w-bold); letter-spacing: normal;
  padding-left: 8px; border-left: 1px solid color-mix(in srgb, var(--bg) 35%, transparent);
  opacity: .78;
}
@media (max-width: 480px) { .boxbadge .bb-id { display: none; } }
.crumb .slash { color: var(--alu); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.clock { font-size: var(--t-small); color: var(--ink-60); font-variant-numeric: tabular-nums; }

/* ---------- page scaffolding ---------- */
.main { flex: 1; }
.page { max-width: 1080px; padding: 36px 40px 72px; }
.h1 {
  font-weight: var(--w-display); font-size: var(--t-title-1); line-height: 1.08;
  letter-spacing: -.015em; text-wrap: balance;
}
.sub { margin-top: 10px; font-size: var(--t-body); color: var(--ink-60); max-width: 60ch; }
.sub .sep { color: var(--alu); padding: 0 6px; }
.sect { margin-top: 52px; display: flex; align-items: baseline; gap: 14px; }
/* `first` = the first section on a page, directly under the h1's sub line. It
   still needs air — it is a new section, not a caption. */
.sect.first { margin-top: 36px; }
.sect h2 { font-weight: var(--w-regular); font-size: var(--t-title-3); letter-spacing: -.01em; }
.sect .line { flex: 1; height: 1px; background: var(--alu-soft); align-self: center; }
/* A control in the section head hangs off the rule, not off the baseline of
   the heading — baseline-aligning a padded button lifts it above the rule. */
.sect .btn { align-self: center; }
.sect .more { font-size: var(--t-small); color: var(--ink-40); text-decoration: none; }
.sect .more:hover { color: var(--ink); }

/* ---------- calm amber notice ----------
   ⛔ NOT an offline banner — there is no longer one of those. This is for a
   screen with something real to say about a feature (first-run uses it to say
   the internet is optional). Same border weight as any other panel, no icon of
   alarm: amber here means "worth knowing", never "wrong". */
.notice {
  display: flex; align-items: center; gap: 12px;
  border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent); background: var(--amber-tint);
  border-radius: var(--r-md); padding: 11px 16px;
  font-size: var(--t-small); color: var(--ink-60); margin-bottom: 32px;
}
.notice b { color: var(--amber); font-weight: var(--w-medium); }
/* ⛔ .notice carries margin-bottom but no margin-top, because it is
   normally the FIRST thing in a section. Following a block it welded to
   the element above with no gap — founder spotted it on the Kits screen,
   2026-09-02. Scoped to the following-sibling case so existing uses that
   open a section keep their spacing exactly as it is. */
.cards + .notice, .band + .notice, .card + .notice { margin-top: 24px; }

/* ---------- home: the check grid ----------
   A hairline-ruled table, brochure-style — deliberately NOT floating cards.
   One outer rule, inner hairlines, no shadow, no gap. */
.checks {
  margin-top: 24px;
  display: grid; grid-template-columns: repeat(3, 1fr);
  border: 1px solid var(--alu); border-radius: var(--r-md);
  overflow: hidden; background: var(--paper-raise);
}
.check {
  padding: 20px 22px 22px; min-height: 128px;
  border-right: 1px solid var(--alu-soft); border-bottom: 1px solid var(--alu-soft);
  display: flex; flex-direction: column; gap: 10px;
}
.check:nth-child(3n) { border-right: none; }
.check:nth-child(n+4) { border-bottom: none; }
.check-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.check-name {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-60);
}
.check-val {
  font-weight: var(--w-display); font-size: var(--t-title-3); letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
.check-note { font-size: var(--t-small); color: var(--ink-40); line-height: 1.45; }
/* Storage tile: free | used side by side, then the used/total meter. */
.check-split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.check-half { display: flex; flex-direction: column; gap: 2px; }
.check-half-label { font-size: var(--t-small); color: var(--ink-40); }

/* ---------- activity rows ---------- */
.rows {
  margin-top: 20px; border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); overflow: hidden;
}
.row {
  display: flex; align-items: center; gap: 16px;
  padding: 14px 20px; border-bottom: 1px solid var(--alu-soft); font-size: var(--t-small);
}
.row:last-child { border-bottom: none; }
.row .t {
  color: var(--ink-40); font-size: var(--t-small);
  font-variant-numeric: tabular-nums; width: 52px; flex: 0 0 auto;
}
/* ⚠️ 106, NOT 92. "NOT CHECKED" — the longest word the health rows put here,
   and the one an owner most needs to read — is ~104px at 11px uppercase with
   .1em tracking, so at 92px it ran into the gap on every width and touched the
   sentence beside it at 4K (measured 2026-09-30: 13px over at 1024, 16px over
   at 3840). The wide-glass tiers below are widened by the same margin. */
.row .k { width: 106px; flex: 0 0 auto; }
.row .what { flex: 1; min-width: 0; color: var(--ink-60); }
.row .what b { color: var(--ink); font-weight: var(--w-regular); }
.day {
  padding: 9px 20px; font-size: var(--t-caption); font-weight: var(--w-medium);
  letter-spacing: normal; text-transform: none; color: var(--ink-40);
  background: var(--paper-deep); border-bottom: 1px solid var(--alu-soft);
}

/* ---------- composer / ask ---------- */
.composer {
  display: flex; align-items: flex-end; gap: 12px;
  border: 1px solid var(--alu); border-radius: var(--r-lg);
  background: var(--paper-raise); padding: 8px 8px 8px 20px;
  box-shadow: 0 1px 0 rgba(0,0,0,.03);
}
.composer input, .composer textarea {
  flex: 1; border: none; background: transparent; font-size: var(--t-body);
  resize: none; padding: 9px 0; line-height: 1.5; max-height: 160px;
}
.composer input::placeholder, .composer textarea::placeholder { color: var(--ink-40); }
.composer input:focus, .composer textarea:focus { outline: none; }
.composer:focus-within { border-color: var(--ink); }

/* .btn — the v1 button lived here (opacity hover, 13.5px, tracked). Retired in
   UI phase 1: ONE button is defined in ui/css/v2.css (RULEBOOK §4, S-04). */

.thread { max-width: 760px; display: flex; flex-direction: column; gap: 26px; margin-top: 34px; }
.msg-q {
  align-self: flex-end; max-width: 80%;
  background: var(--paper-deep); border: 1px solid var(--alu-soft);
  border-radius: var(--r-md) var(--r-md) var(--r-sm) var(--r-md); padding: 12px 18px; font-size: var(--t-body);
  white-space: pre-wrap;
}
.msg-a { max-width: 92%; display: flex; flex-direction: column; gap: 12px; }
.msg-a .who {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
}
/* ⚠️ NOT pre-wrap any more (2026-09-29). The answer is rendered markdown
   (js/screens/ask.js paint → gpt/render.js), so line breaks are <br>, <p> and
   blocks, and pre-wrap would double every one of them. Only <pre> keeps its
   whitespace, below. The 64ch cap went with it: a comparison table needs the
   column, and the thread is already capped at 760px. */
.msg-a .body {
  font-size: var(--t-body); line-height: 1.65; color: var(--ink); max-width: 100%;
  overflow-wrap: anywhere;
}
.msg-a.thinking .body::after {
  content: "▌"; color: var(--ink-40); animation: blink 1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }

/* Source chips. The presentation is kept from the template; nothing renders
   unless /v1/chat sends sources — it does since 2026-10-03, only when documents
   were handed to the model. See js/screens/ask.js. */
.srcs { display: flex; gap: 8px; flex-wrap: wrap; }
.src {
  display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid var(--alu-soft); border-radius: var(--r-pill); background: var(--paper-raise);
  padding: 4px 12px; font-size: var(--t-caption); color: var(--ink-60); text-decoration: none;
}
.src:hover { border-color: var(--ink); }

.ask-wrap { max-width: 760px; }
.ask-foot {
  margin-top: 14px; display: flex; align-items: center; gap: 8px;
  font-size: var(--t-caption); color: var(--ink-40);
}
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.chip {
  border: 1px solid var(--alu-soft); border-radius: var(--r-pill); padding: 7px 15px;
  font-size: var(--t-small); color: var(--ink-60); background: var(--paper-raise);
}
.chip:hover { border-color: var(--ink); color: var(--ink); }
.chip[aria-pressed="true"] { border-color: var(--ink); color: var(--ink); background: var(--paper-deep); }
.chip:disabled { opacity: .45; cursor: default; }
.chip:disabled:hover { border-color: var(--alu-soft); color: var(--ink-60); }

/* ---------- ask: the thread list ----------
   Conversations beside the thread on the glass; folded behind a button at
   phone width, because a 375px screen behind a 64px rail leaves ~311px and
   .thread already takes all of it (the 760px block below drops its max-width
   for the same reason). See js/screens/ask.js. */
.ask-layout {
  display: grid; grid-template-columns: 236px minmax(0, 1fr); gap: 34px;
  margin-top: 26px; align-items: start;
}
.ask-side { position: sticky; top: 24px; display: flex; flex-direction: column; gap: 12px; }
/* min-width:0 because a grid item defaults to min-width:auto, and the
   composer's textarea has an intrinsic width that then widens the column
   past a phone's viewport instead of shrinking to it. */
.ask-main { min-width: 0; }
.ask-side__head { display: flex; gap: 8px; flex-wrap: wrap; }
.ask-side__toggle { display: none; }
.convs { display: flex; flex-direction: column; gap: 2px; }
/* The day headings — Today / Yesterday / Previous 7 days / Earlier — the
   shape every chat product's sidebar has taught people to expect. */
.convs-group {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40); margin: 14px 12px 4px;
}
.convs-group:first-child { margin-top: 2px; }
/* A row is an OPEN button and a small DELETE button side by side (a button
   cannot contain a button). The delete is faint until the row is hovered or
   focused, and always visible on the open row — the one somebody is most
   likely to be finished with. See js/screens/ask.js askToDelete. */
.conv {
  display: flex; align-items: center; gap: 4px;
  border: 1px solid transparent; border-radius: var(--r-md); background: transparent;
}
.conv:hover, .conv:focus-within { background: var(--paper-deep); }
.conv[aria-current="true"] { background: var(--paper-raise); border-color: var(--alu); }
.conv__open {
  flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  text-align: left; padding: 9px 4px 9px 12px; background: transparent; border: 0;
}
.conv__title {
  font-size: var(--t-small); color: var(--ink); max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.conv__when { font-size: var(--t-caption); color: var(--ink-40); }
.conv__del {
  flex: 0 0 auto; width: 28px; height: 28px; margin-right: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-md); background: transparent; color: var(--ink-40);
  opacity: 0; transition: opacity .12s;
}
.conv:hover .conv__del, .conv:focus-within .conv__del, .conv[aria-current="true"] .conv__del,
.conv__del:focus-visible { opacity: 1; }
.conv__del:hover { color: var(--red); background: var(--red-tint); }
/* Mid-confirm the row becomes the question. Red edge, because this is the one
   irreversible act on the screen, and the title is quoted so the second press
   is about a named thing. */
.conv.is-confirming { border-color: var(--red); background: var(--paper-raise); display: block; }
.conv__confirm { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
.conv__confirm-q { font-size: var(--t-small); color: var(--ink); line-height: 1.45; overflow-wrap: anywhere; }
.conv__confirm-q b { font-weight: var(--w-medium); }
.conv__confirm-acts { display: flex; gap: 6px; }
.conv__confirm .conv-err { flex-basis: auto; font-size: var(--t-caption); }
.convs-state { font-size: var(--t-small); color: var(--ink-60); line-height: 1.5; padding: 4px 2px; }
.convs-state p { margin: 0 0 8px; }
.convs-state b { color: var(--ink); font-weight: var(--w-medium); }
.conv-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; }
.conv-title {
  font-size: var(--t-lead); font-weight: var(--w-medium); letter-spacing: -.01em;
  flex: 1; min-width: 0; overflow-wrap: anywhere;
}
.conv-acts { display: flex; gap: 6px; }
.conv-rename { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.conv-rename input {
  flex: 1 1 200px; min-width: 0; font-size: var(--t-body); padding: 8px 12px;
  background: var(--paper-raise); border: 1px solid var(--alu); border-radius: var(--r-md);
}
.conv-rename input:focus { outline: none; border-color: var(--ink); }
.conv-err { flex: 1 0 100%; margin-top: 0; color: var(--red); }
.conv-err:empty { display: none; }
#keep-note:empty { display: none; }

/* ---------- cards ---------- */
.cards { margin-top: 24px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.card {
  border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); padding: 22px;
  display: flex; flex-direction: column; gap: 12px;
}
.card.sel { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.card h3 { font-weight: var(--w-medium); font-size: var(--t-body); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card .desc { font-size: var(--t-small); color: var(--ink-60); line-height: 1.5; flex: 1; }
.card .meta { font-size: var(--t-caption); color: var(--ink-40); }
.card .acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* ---------- setting rows ---------- */
.setgroup {
  margin-top: 22px; border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); overflow: hidden;
}
.setrow {
  display: flex; align-items: center; gap: 20px;
  padding: 17px 22px; border-bottom: 1px solid var(--alu-soft);
}
.setrow:last-child { border-bottom: none; }
.setrow .info { flex: 1; min-width: 0; }
/* ⛔ DIRECT CHILD, not any descendant. `.setrow .info b` also matched a <b>
   used for emphasis INSIDE the description span and turned it into a block,
   snapping the sentence in half mid-clause. Found by rendering, 2026-09-02. */
.setrow .info > b { display: block; font-weight: var(--w-medium); font-size: var(--t-body); }
.setrow .info span { display: block; font-size: var(--t-small); color: var(--ink-60); margin-top: 2px; max-width: 62ch; }
.setrow .val {
  font-size: var(--t-small); color: var(--ink-60);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* ⛔ A ROW THAT GROWS A THIRD CHILD MUST BE TOLD IT MAY WRAP, and the place to
   say so is here, beside the `display:flex` it is arguing with. The Wi-Fi join
   draws its password field INTO the row (deliberately — a modal on a
   wall-mounted screen is a modal somebody has to dismiss with a mouse they may
   not have), and `.setrow` has no `flex-wrap`. The field therefore competed for
   the row's single line with the label and the Connect button, and the founder
   saw them overlapping on RH002's glass on 2026-09-28. An inline
   `flex-basis:100%` in the injected markup could not fix it: basis is not
   permission to take a new line.
   ⚠️ SCOPED TO `.asks` ON PURPOSE. Making every `.setrow` wrap would relayout
   every settings row in the product to fix one, which is the trade that breaks
   four screens to mend one. */
.setrow.asks { flex-wrap: wrap; row-gap: 12px; }
.setrow.asks .pw { flex-basis: 100%; }
.setrow .pw input {
  width: 100%; max-width: 22rem; box-sizing: border-box;
  padding: 9px 12px; font-size: var(--t-small);
  border: 1px solid var(--alu-soft); border-radius: var(--r-sm);
  /* the token comment calls --paper-raise the raised surface: rows, cards, composer */
  background: var(--paper-raise); color: inherit;
}
.setrow .pw input:focus-visible { outline: 2px solid var(--ink-60); outline-offset: 1px; }

/* ==========================================================================
   ⭐ BUILD STREAM A — POWER, RESTART, CLOCK, VOLUME.
   ⛔ PLACED HERE, IMMEDIATELY BELOW .setrow, AND THE FIRST PLACE WAS WRONG.
   This block was originally appended to the END of the file on the reasoning
   that the end of a file is where a block cannot collide. A trial merge
   against the sibling files stream (git merge-tree, nothing checked out)
   showed that IS the conflict: the other developer reached for the same safe
   place, and two appends to one file is one conflict. Moved next to the rules
   it extends — these two controls are .setrow children and nothing else uses
   them — which is both topically right and, measured, conflict-free.

   ⛔ TWO CONTROLS THE INTERFACE HAD NEVER NEEDED BEFORE, and that absence is
   worth noting rather than rounding off: before this change there was not a
   single <select> or range input anywhere in ui/ — every screen was buttons
   and text. These follow the same paper-light treatment as .btn.ghost so the
   product does not suddenly show a native operating-system widget, which on
   an appliance whose entire thesis is "the owner never administers Linux"
   would read as a piece of the host leaking through.
   ========================================================================== */

.oxo-select {
  font: inherit;
  font-size: var(--t-small);
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--alu);
  border-radius: var(--r-md);
  padding: 7px 10px;
  max-width: 220px;
  /* The native arrow stays: a picker that does not look openable is a picker
     nobody opens, and this is the one place a familiar control beats a
     bespoke one. */
}
.oxo-select:disabled { opacity: .45; }

.oxo-range {
  /* ⚠️ WIDTH IS CAPPED, NOT FIXED. .setrow is a flex row that must still
     shrink at 375px — the phone work measured nine screens clean at a
     verified 375px and a fixed-width child here would put a horizontal
     scrollbar back on System. */
  flex: 0 1 200px;
  min-width: 100px;
  max-width: 200px;
  accent-color: var(--ink);
  cursor: pointer;
}
.oxo-range:disabled { opacity: .45; cursor: default; }

@media (max-width: 560px) {
  /* At phone width the label, the control and the readout stop fitting on
     one line. The row wraps rather than squeezing the sentence. */
  .setrow:has(.oxo-select), .setrow:has(.oxo-range) { flex-wrap: wrap; }
  .oxo-select { max-width: 100%; }
  .oxo-range { flex: 1 1 100%; max-width: 100%; }
}
/* ============ END BUILD STREAM A ======================================== */

/* ---------- tables ---------- */
.tablewrap {
  margin-top: 20px; border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); overflow-x: auto;
  /* ⛔ overflow-x:auto ALONE DID NOT STOP THE PAGE SCROLLING SIDEWAYS, AND THE
     SCREENSHOT SAID IT HAD. Measured at 375px on 2026-09-15: the Files screen
     moved the whole page 213px to the right (root scrollWidth 588 against a
     375 viewport) even though this wrap was the correct 267px wide and the
     table was already scrolling inside it. The table's layout overflow was
     propagating past the scroll container to the root scroller, so the owner
     got BOTH a table that scrolls and a page that slides — and the second one
     drags the topbar and the rail off the screen with it.
     `contain: layout` states the thing that is actually true — nothing inside
     this box participates in layout outside it — and that is what stops the
     propagation. Chosen over `overflow-x: clip` on .page or .main, which both
     also fix it but put a clip on the whole scroll region and would silently
     break position:sticky and focus rings for every screen, not just this one.
     Isolated by test, not reasoned about: removing the table, and setting
     table-layout:fixed, each took the page back to 375; `contain` on the wrap
     does it without touching the column widths.
     ⚠️ Verified in Blink only. If WebKitGTK ignores `contain`, this degrades to
     exactly today's behaviour (the page slides) — it cannot break anything. */
  contain: layout;
}
table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
th {
  text-align: left; padding: 11px 20px; font-size: var(--t-caption); font-weight: var(--w-medium);
  letter-spacing: normal; text-transform: none; color: var(--ink-40);
  border-bottom: 1px solid var(--alu); background: var(--paper-deep); white-space: nowrap;
}
td { padding: 13px 20px; border-bottom: 1px solid var(--alu-soft); white-space: nowrap; color: var(--ink-60); }
td:first-child { color: var(--ink); }
tr:last-child td { border-bottom: none; }
td .num, .num { font-variant-numeric: tabular-nums; }

/* The dashed frame the template used for a drop zone. It frames the real
   "add a file" control rather than pretending to be a drop target.
   ⚠️ IT IS STILL NOT A DROP TARGET. There is an upload endpoint now
   (POST /v1/files) but no drop handler, so a dragged file would still land on
   nothing. Wiring `dragover`/`drop` to the same api.upload() call is a small
   follow-up and is NOT done here — named so the dashed border is not read as a
   promise a second time. */
.dropframe {
  margin-top: 16px; border: 1px dashed var(--alu); border-radius: var(--r-md);
  padding: 22px; display: flex; flex-direction: column; gap: 12px;
}
.dropframe input {
  flex: 1; background: var(--paper-raise); border: 1px solid var(--alu);
  border-radius: var(--r-md); padding: 10px 14px;
}

/* ---------- stat band ---------- */
.band {
  margin-top: 24px; display: grid; grid-template-columns: repeat(4, 1fr);
  border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); overflow: hidden;
}
.band.three { grid-template-columns: repeat(3, 1fr); }
.stat { padding: 18px 22px; border-right: 1px solid var(--alu-soft); }
.stat:last-child { border-right: none; }
.stat .label {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
}
.stat .value {
  margin-top: 6px; font-weight: var(--w-display); font-size: var(--t-title-1);
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.stat .value.words { font-size: var(--t-title-3); line-height: 1.5; letter-spacing: 0; }
.stat .value small { font-size: var(--t-small); font-weight: var(--w-regular); color: var(--ink-60); letter-spacing: 0; }
.stat .value.is-local { color: var(--local); }
.stat .value.is-amber { color: var(--amber); }
.stat .foot { margin-top: 6px; font-size: var(--t-caption); color: var(--ink-40); }

/* ---------- meters ---------- */
.meter { height: 3px; border-radius: var(--r-sm); background: var(--paper-deep); overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--ink); border-radius: var(--r-sm); }
.meter.g i { background: var(--local); }

/* ---------- progress (installs, indexing) ---------- */
.progress { height: 3px; background: var(--paper-deep); border-radius: var(--r-sm); overflow: hidden; margin-top: 12px; }
.progress > div { height: 100%; background: var(--ink); border-radius: var(--r-sm); transition: width .3s; }
.progress-label { margin-top: 8px; font-size: var(--t-small); color: var(--ink-60); }

/* ---------- the five states ----------
   loading / empty / error each get a panel; offline gets .notice above,
   because offline does not replace a screen — the screen still works. */
.state-panel {
  border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise);
  padding: 48px 40px; text-align: center; max-width: 680px; margin: 32px 0;
}
.state-panel h2 { font-weight: var(--w-regular); font-size: var(--t-title-2); letter-spacing: -.01em; margin-bottom: 8px; }
.state-panel p { color: var(--ink-60); font-size: var(--t-small); max-width: 52ch; margin: 0 auto 20px; }
.state-panel p:last-child { margin-bottom: 0; }
.state-error { border-color: color-mix(in srgb, var(--bad) 30%, transparent); background: var(--red-tint); }
.state-error h2 { color: var(--red); }

.spinner {
  width: 26px; height: 26px; margin: 0 auto 22px; border-radius: 50%;
  border: 2px solid var(--alu-soft); border-top-color: var(--ink);
  animation: spin .9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- engineers' disclosure ---------- */
details.tech { margin-top: 16px; border-top: 1px solid var(--alu-soft); padding-top: 12px; }
details.tech summary {
  cursor: pointer; color: var(--ink-40); font-size: var(--t-small); font-weight: var(--w-medium);
  list-style: none; display: inline-flex; align-items: center; gap: 8px;
}
details.tech summary::-webkit-details-marker { display: none; }
details.tech summary::before { content: "\25B8"; }
details.tech[open] summary::before { content: "\25BE"; }
details.tech .tech-body {
  margin-top: 12px; font-family: var(--mono); font-size: var(--t-caption); color: var(--ink-60);
  background: var(--paper-deep); border-radius: var(--r-md); padding: 14px 16px;
  overflow-wrap: anywhere;
}
.tech-body dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 18px; margin: 0; }
.tech-body dt { color: var(--ink-40); }
.tech-body dd { margin: 0; }

/* ============================== first run ==============================
   A fixed overlay over the shell. Same paper, same rules — a first-run that
   looked like a different product would be the worst first impression there
   is. */
.fr { position: fixed; inset: 0; background: var(--paper); z-index: 20; display: flex; flex-direction: column; overflow-y: auto; }
.fr-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 40px; text-align: center; }
.fr-body h1 { font-weight: var(--w-display); font-size: var(--t-title-1); line-height: 1.08; letter-spacing: -.015em; margin-bottom: 14px; }
.fr-lede { color: var(--ink-60); font-size: var(--t-lead); max-width: 60ch; margin: 0 auto 34px; }
.fr-card {
  border: 1px solid var(--alu); border-radius: var(--r-md); background: var(--paper-raise);
  padding: 30px 36px; min-width: 560px; max-width: 720px; text-align: left;
}
.fr-foot { display: flex; justify-content: center; gap: 8px; padding: 26px; }
.fr-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--alu-soft); }
.fr-dot.done { background: var(--ink); }
.fr-nav { display: flex; justify-content: center; gap: 14px; margin-top: 34px; }
.fr-field { margin-bottom: 18px; }
.fr-field label { display: block; font-weight: var(--w-medium); margin-bottom: 8px; font-size: var(--t-body); }
.fr-field input {
  width: 100%; background: var(--paper); border: 1px solid var(--alu);
  border-radius: var(--r-md); padding: 12px 14px;
}
.check-list { list-style: none; text-align: left; }
.check-list li { display: flex; gap: 14px; align-items: center; padding: 10px 0; font-size: var(--t-lead); }
.privacy-hero { font-weight: var(--w-display); font-size: var(--t-title-1); color: var(--local); margin: 10px 0 18px; }

/* shared small bits used by screens */
.field-row { display: flex; gap: 12px; align-items: center; }
.field-row input { flex: 1; background: var(--paper-raise); border: 1px solid var(--alu); border-radius: var(--r-md); padding: 10px 14px; }
/* ⛔ AN INPUT DOES NOT GIVE GROUND JUST BECAUSE YOU GAVE IT flex:1.
   A flex item's default min-width is auto, and on a text input that resolves to
   the control's intrinsic size (~170px in this stack) — so `flex: 1` above let
   it GROW and never let it shrink. Sat beside a nowrap .btn that pushed
   "Share with my AI" (Files) and "Add someone" (People) clean off a 375px
   screen: measured right edges of 462px and 443px on 2026-09-15. These two rows
   are the ONLY page-level overflows at 375px that are not the topbar, and they
   are one shared row component, so this is one fix and not two.
   Below 480 the pair STACKS rather than shrinking further: a folder path
   squeezed into what is left beside a button is a field nobody can read, which
   is the same call the Activity row and the peer row already made further down
   this file. */
.field-row input { min-width: 0; }
@media (max-width: 480px) {
  .field-row { flex-wrap: wrap; }
  .field-row input { flex: 1 0 100%; }
  .field-row .btn { width: 100%; }
}
.hint { font-size: var(--t-small); color: var(--ink-40); margin-top: 8px; }
.actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
.stack-gap { margin-top: 16px; }

/* ---------- People: the joining code ----------
   The code is read ACROSS A ROOM, possibly aloud to a colleague. Display
   size, 300 weight (the brand's numeral voice), two groups of three for
   read-aloud rhythm, tabular figures so nothing shimmies while the clock
   line under it updates. Ink on raised paper: 16.85:1. */
.join-card {
  border: 1px solid var(--alu); border-radius: var(--r-lg);
  background: var(--paper-raise); text-align: center;
  padding: 54px 44px 44px; max-width: 880px; margin-top: 30px;
}
.join-kicker {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  color: var(--ink-40); margin-bottom: 10px;
}
.join-card h2 { font-weight: var(--w-display); font-size: var(--t-title-1); letter-spacing: -.015em; margin-bottom: 24px; }
.join-step { color: var(--ink-60); font-size: var(--t-lead); max-width: 56ch; margin: 0 auto; }
.join-addr {
  font-weight: var(--w-display); font-size: var(--t-title-1); letter-spacing: .01em;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; margin: 8px 0 24px;
}
.joincode {
  display: flex; justify-content: center; align-items: center;
  font-weight: var(--w-display); font-size: 112px; line-height: 1; letter-spacing: .12em;
  font-variant-numeric: tabular-nums; margin: 14px 0 4px;
}
.joincode .gap { width: .55em; }
.join-clock { margin-top: 18px; color: var(--ink-60); font-size: var(--t-body); }

/* ---------- People: the square a camera reads ----------
   ⛔ THE WHITE TILE IS NOT A THEME SLIP. Every other surface here is ink on
   raised paper; this one is deliberately a light card with a quiet zone,
   because a camera needs dark modules on a light field and the specification
   requires the margin. It is the one element on the glass that is drawn for a
   machine rather than for a person.
   Sized so the picture is comfortably readable from arm's length across a
   desk at 240px, and still fits a 375px phone at 200px. */
.join-scan { margin: 4px 0 26px; }
.join-qr {
  display: inline-block; background: #fff; padding: 10px;
  border-radius: var(--r-md); border: 1px solid var(--alu);
  line-height: 0; margin-bottom: 16px;
}
.join-qr svg { width: 240px; height: 240px; display: block; }
.join-or {
  color: var(--ink-40); font-size: var(--t-caption); letter-spacing: normal;
  text-transform: none; margin: 0 0 18px;
}

.join-acts { display: flex; justify-content: center; gap: 12px; margin-top: 30px; flex-wrap: wrap; }
.join-ok { color: var(--local); font-size: var(--t-display); font-weight: var(--w-display); line-height: 1; margin-bottom: 12px; }
/* a device row sits inside its person's group, visibly subordinate */
.setrow.dev { padding-left: 28px; }

/* ---------- Computers: a row about another machine ----------
   Appended at the end of the People block on purpose: a computer joins the
   same way a person's device does — a code on one screen typed on the other —
   so it reuses .setgroup / .setrow / .join-card / .joincode wholesale. What is
   added here is layout only.
   ⛔ NO NEW HUE AND NO NEW TINT. The three status colours are the ones already
   in tokens.css; "Heard" is --ink-40 on paper (4.59:1 at 11px uppercase, which
   is not large text, so the bar is 4.5:1) and no darker ground is introduced
   under it. Iris §10a. */
.setrow.peer { align-items: flex-start; gap: 14px; }
/* The Ox1 mark is PLACED, never drawn, recoloured, stretched or cropped
   (BRAND-GUIDE 1/2/4). Fixed box, no filter, black master on light paper —
   the currentColor variant only themes when inlined, and this is an <img>. */
.peer-mark { width: 22px; height: 22px; flex: 0 0 auto; margin-top: 3px; display: block; }
/* ⛔ THE TAG HAS TO BE PUT BACK INLINE, AND IT COST TWO RENDERS TO SEE WHY.
   `.setrow .info span { display: block }` (written for the description line)
   matches EVERY span inside .info — which includes .tag and the .dot inside
   it, both of which are spans. So a name line reading
   "Reception · Main · Ready" came out as three stacked lines with the status
   dot on a line of its own. Specificity, not !important, and scoped to this
   screen's rows so nothing else moves. `vertical-align: middle` because an
   inline-flex box takes its baseline from its first flex item, and the first
   one here is the dot — which has no text and therefore no baseline. */
.setrow.peer .info > b { line-height: 1.6; }
.setrow.peer .info > b .tag {
  display: inline-flex; vertical-align: middle; margin-left: 10px;
}
.setrow.peer .info > b .tag .dot { display: inline-block; }
.setrow.peer .info span { margin-top: 4px; }
.peer-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
/* A two-press confirm grows its own label to a whole sentence; the button may
   wrap rather than widen the row past the viewport. */
.peer-acts .btn { white-space: normal; text-align: center; max-width: 30ch; }
.sect-note {
  align-self: center; font-size: var(--t-caption); letter-spacing: normal;
  text-transform: none; color: var(--ink-40); white-space: nowrap;
}
/* …but a sect-note that is a PARAGRAPH (Settings › Intelligence Fleet › Shared
   with your fleet) is a sentence, and a nowrap sentence made that page 599 px
   wide on a 390 px phone. Found by tests/ui/touch-targets.test.mjs. */
p.sect-note { white-space: normal; }
/* .notice carries margin-bottom but deliberately no margin-top (it normally
   opens a section), so a notice that follows a group needs its own air. */
.peer-notice { margin-top: 20px; }

/* ---------- Computers: WHAT A COMPUTER HOLDS (ADR-0015 slice A) ----------
   The founder, 2026-09-17: "I don't want to be connecting to two machines, but
   I want to know there are two machines... as I may have different models on
   different boxes." This is that answer, one block per computer.
   ⛔ NO NEW HUE, NO NEW TOKEN, NO NEW TINT. Label/value pairs on the same
   paper the rest of the row sits on, ruled with --alu-soft like every other
   inner rule in this file. The only colour in here is the one --ink-40 the
   micro-labels already use. Nothing for the token registry to find. */
.peer-holds {
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--alu-soft);
  display: grid; grid-template-columns: max-content 1fr;
  column-gap: 16px; row-gap: 6px; max-width: 62ch;
}
/* ⛔ SPECIFICITY, NOT !important, AND IT IS THE SAME TRAP AS THE TAG ABOVE.
   `.setrow .info span { display: block; margin-top: 2px }` matches every span
   in here, which would put each label on its own line and undo the grid. */
.setrow.peer .info .peer-holds dt,
.setrow.peer .info .peer-holds span.k {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  color: var(--ink-40); margin-top: 3px; white-space: nowrap;
}
.setrow.peer .info .peer-holds span.v {
  font-size: var(--t-small); color: var(--ink-60); margin-top: 0; max-width: none;
}
/* ⛔ THE THIRD STATE, AND IT IS A WORD NOT A DASH. "Not checked" is amber
   everywhere else in this product and it is amber here. An em dash would read
   as "none", which is the exact confusion rule 4 exists to stop. */
.setrow.peer .info .peer-holds span.v.unlooked { color: var(--amber); }
@media (max-width: 760px) {
  /* At 375px behind a 64px rail a max-content label column plus a value column
     leaves ~10ch for the value, which wraps "Everyday AI, Reasoning AI" into a
     vertical ribbon. The pairs stack instead; the label keeps its own line and
     stays a label because it stays uppercase micro. */
  .peer-holds { grid-template-columns: 1fr; row-gap: 2px; }
  .setrow.peer .info .peer-holds span.k { margin-top: 10px; }
  .setrow.peer .info .peer-holds span.k:first-child { margin-top: 0; }
}
/* The six characters typed on the initiating screen. The field is centred and
   sized for six characters, not for a sentence — it is the same act as the
   /join page's code box, at the glass's own scale. */
.code-entry { max-width: 320px; margin: 22px auto 0; text-align: center; }
.code-entry label { display: block; margin-bottom: 8px; }
.code-entry input {
  width: 100%; background: var(--paper); border: 1px solid var(--alu);
  border-radius: var(--r-md); padding: 12px 14px; text-align: center;
  font-size: 26px; font-weight: var(--w-display); letter-spacing: .28em; text-indent: .28em;
  font-variant-numeric: tabular-nums; text-transform: uppercase;
}
.code-entry .hint, .code-entry .lookagain { text-align: center; margin-top: 8px; }
/* Amber, because a mistyped character is worth knowing and is never "wrong". */
.lookagain { font-size: var(--t-caption); color: var(--amber); margin-top: 8px; }
.code-entry .btn { margin-top: 18px; }
.join-card .notice { margin: 20px 0 0; text-align: left; }
/* The Forget done-view's four answers. A .join-card centres everything, which
   is right for one sentence and wrong for four. */
.join-facts { text-align: left; max-width: 58ch; margin: 24px auto 0; display: grid; gap: 14px; }
.join-facts .join-step { margin: 0; max-width: none; }
@media (max-width: 760px) {
  /* ⛔ A SECTION HEAD WITH A CONTROL IN IT HAS TO WRAP ON A PHONE. `.sect` is
     one flex line and `.sect-note` is nowrap by design (it is a status, and a
     status broken across two lines mid-word reads as two statuses). At 375px
     behind a 64px rail that put "Ready to join · 9 min left" plus a button
     past the right edge. Wrapping the LINE, not the words, keeps both. */
  .sect { flex-wrap: wrap; row-gap: 8px; }
  .sect .line { min-width: 20px; }
  /* The row STACKS on a phone rather than shrinking: a computer's name, its
     tag and two buttons cannot share 375px behind a 64px rail. Same lesson as
     the Activity row further down this file. */
  .setrow.peer { flex-wrap: wrap; }
  .peer-acts { width: 100%; justify-content: flex-start; padding-left: 36px; }
  .code-entry input { font-size: 22px; letter-spacing: .2em; text-indent: .2em; }
}
@media (max-width: 960px) {
  .joincode { font-size: 76px; }
  .join-addr { font-size: var(--t-title-2); }
}

/* ============================== responsive ============================== */
@media (max-width: 960px) {
  .checks { grid-template-columns: 1fr 1fr; }
  .check:nth-child(3n) { border-right: 1px solid var(--alu-soft); }
  .check:nth-child(2n) { border-right: none; }
  .check:nth-child(n+4) { border-bottom: 1px solid var(--alu-soft); }
  .check:nth-child(n+5) { border-bottom: none; }
  .cards, .band, .band.three { grid-template-columns: 1fr 1fr; }
  .stat:nth-child(2n) { border-right: none; }
  .fr-card { min-width: 0; width: 100%; }
}
@media (max-width: 760px) {
  .rail { width: 64px; padding: 22px 10px; }
  /* ⛔ The mark is HIDDEN, not shrunk. Brand rule 6 sets a 140px floor for a
     full lock-up and the pack ships no standalone-emblem master, so there is
     nothing legal to put in a 64px rail. Named as a brand-pack gap in the
     handback rather than solved by cropping someone else's artwork. */
  .rail .brand, .rail .brand-tag, .rail .rail-meta { display: none; }
  /* ⛔ The nav labels are hidden VISUALLY, not removed. `display:none` takes
     them out of the accessibility tree too, which leaves six icon-only links
     with no accessible name at all — WCAG 2.1 AA 4.1.2 and 2.4.4. Clipping
     keeps the name for a screen reader and for the tooltip. */
  .rail .nav a span {
    position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .nav { margin-top: 0; }
  .nav a { justify-content: center; padding: 11px 0; }
  .page { padding: 28px 22px 60px; }
  .topbar { padding: 14px 22px; }
  .h1 { font-size: var(--t-title-1); }
  .checks { grid-template-columns: 1fr; }
  .check { border-right: none !important; border-bottom: 1px solid var(--alu-soft) !important; }
  .check:last-child { border-bottom: none !important; }
  .cards, .band, .band.three { grid-template-columns: 1fr; }
  .stat { border-right: none; border-bottom: 1px solid var(--alu-soft); }
  .stat:last-child { border-bottom: none; }
  .row .k { width: auto; }
  /* ⚠️ A long nowrap value beside a pill left the row's sentence ~25ch wide
     at 760 (System › Privacy › "Reaching out" wrapped to five lines beside
     "changed on this computer, not from a screen"). The value may wrap here;
     the pill and the sentence keep their places. Rendered 2026-09-30. */
  .setrow .val { white-space: normal; text-align: right; max-width: 38%; }
  .thread, .ask-wrap { max-width: none; }
  /* One column; the list folds behind its button until asked for. */
  .ask-layout { grid-template-columns: 1fr; gap: 18px; }
  .ask-side { position: static; }
  .ask-side__toggle { display: inline-flex; }
  .ask-side:not(.open) .convs { display: none; }
  .fr-card { padding: 22px 20px; }
  .joincode { font-size: 52px; letter-spacing: .1em; }
  .join-card { padding: 34px 20px 30px; }
  .join-qr svg { width: 200px; height: 200px; }
}

/* ⛔ THE ROW OVERFLOWED THE PHONE, AND ACTIVITY IS A SHIPPED SCREEN.
   `.row` is one flex line: .t (52px) + .k (92px) + two 16px gaps + 40px of
   padding is 216px of fixed chrome. On a 375px screen behind a 64px rail that
   leaves the description about 29px, so it ran off the side of the display.
   The 760px block above set `.row .k { width: auto }`, which helps a tablet and
   does nothing here — the row has to STACK, not shrink.

   Rendered at 375px before and after rather than reasoned about. Time and kind
   keep their line because they are short and scannable; the description gets
   the full width underneath, which is the order you read them in anyway. */
@media (max-width: 480px) {
  /* ⚠️ THE SETTINGS ROW STACKS ON A PHONE. Measured through the 375 frame on
     2026-09-30: beside "Manage computers" (System › Intelligence Fleet) the
     sentence column was ~53px wide and "Your other OxOn computers" ran 15px
     out of it; "Test the AI on its own" the same by 17px. These rows post-date
     the 2026-09-15 phone pass. The sentence takes the whole line and the
     control drops under it — the same call .row and .field-row made here. */
  .setrow { flex-wrap: wrap; row-gap: 10px; }
  .setrow .info { flex: 1 1 100%; }
  .setrow .val { max-width: none; text-align: left; }
  .row {
    flex-wrap: wrap;
    row-gap: 4px;
    padding: 12px 16px;
    align-items: baseline;
  }
  .row .t { width: auto; flex: 0 0 auto; }
  .row .k { flex: 0 0 auto; }
  /* full width on its own line, and min-width:0 so long words can break */
  .row .what { flex: 1 0 100%; min-width: 0; overflow-wrap: anywhere; }
  .day { padding: 8px 16px; }
  /* The conversation's name gets the whole line; Rename and Delete drop
     under it rather than squeezing a three-word title into three rows.
     Seen at 375px with "Supplier quotes for June" beside two buttons. */
  .conv-title { flex: 1 0 100%; }
}

/* ============================== motion ============================== */
@media (prefers-reduced-motion: no-preference) {
  .page { animation: rise .28s ease both; }
  @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
}
@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2.4s; }
  .msg-a.thinking .body::after { animation: none; }
  .progress > div { transition: none; }
}

/* ── Screensaver ────────────────────────────────────────────────────────────
   An Ox1 sits on one screen in one room for years; a static page on an
   unchanging panel burns in. See js/screensaver.js for why the mark BREATHES
   rather than being drawn (the brand pack has no vector master, and the marks
   are placed, never drawn).                                                  */

#screensaver {
  position: fixed;
  inset: 0;
  z-index: 900;                 /* above the app, below nothing that matters */
  background: var(--paper);     /* bone, the product's own ground */
  display: grid;
  place-items: center;
  opacity: 0;
  pointer-events: none;         /* never eats the input that dismisses it */
  transition: opacity 900ms ease;
}
#screensaver.on { opacity: 1; }

/* The APPROVED PNG, used as a mask. The silhouette is entirely the master
   file; only the colour behind it is ours. Nothing is redrawn. */

/* ⛔ MEASURED ON RH001, 2026-09-02 21:10 UTC, AFTER 2h15m OF IDLE:
   this element was burning 95% OF ONE CORE, continuously, doing nothing but
   showing the screensaver — 477 CPU ticks in a 5-second sample read straight
   from /proc/<pid>/stat, with `top` corroborating ~110% on the UI process plus
   ~20% on the WebKit web process and ~10% on cage. 125 minutes of CPU time
   accumulated since the 18:55 restart.

   ⭐ THE IRONY IS THE POINT AND IT IS WHY THIS IS A REAL DEFECT, not a tuning
   nit. This feature exists because the founder said "I hate seeing a page
   burning" — and to stop the panel burning it was burning a core, on an
   appliance whose whole proposition is that it sits silently in someone's room
   for years. Heat, fan noise and 24/7 draw, on an idle machine. NOTHING WE
   MONITOR WOULD EVER HAVE REPORTED IT: every probe was green, because the
   product was working exactly as designed. It was found by looking at the
   actual screen and then asking what the machine was doing behind it.

   THE MECHANISM: `mask` puts this element on a path that will not promote to
   its own compositing layer by default, so `opacity` and `transform` — which
   are normally free — get repainted on the CPU every frame, at 4K, forever.
   The fix is to make the promotion explicit rather than to slow the animation
   down: the design (a heartbeat, a slow drift) is CORRECT and is not what
   costs. Two infinite animations on a promoted layer are close to free.

   ⚠️ The `transition: background` that used to be here is GONE, and its removal
   is not incidental. js/screensaver.js re-tints every 9 s and its own comment
   says "a discrete change at the trough is CALMER than a continuous slide, and
   it is one property write" — the transition contradicted that in the very
   next file, and turned each tint change into 2.4 s of continuous repaint of a
   masked element. The stated design is the cheap one; we were paying for the
   opposite of it.

   ⛔ IF YOU CHANGE THIS BLOCK, RE-MEASURE. Do not trust that it is still cheap:
     u1=$(awk '{print $14+$15}' /proc/<kiosk-pid>/stat); sleep 5
     u2=$(awk '{print $14+$15}' /proc/<kiosk-pid>/stat); echo $(( (u2-u1)*100/500 ))%
   Idle should be single digits. Anything near 100 means the layer stopped
   being promoted and the repaint is back.                                   */
.ss-mark {
  --ss-tint: var(--local);
  width: min(38vw, 420px);
  aspect-ratio: 1 / 1;
  background: var(--ss-tint);
  -webkit-mask: url('../assets/brand/OxOnOS-primary-black-transparent.png')
                center / contain no-repeat;
          mask: url('../assets/brand/OxOnOS-primary-black-transparent.png')
                center / contain no-repeat;
  /* ⛔ THE LAYER-PROMOTION FIX WAS TRIED HERE AND IT DID NOT WORK. Measured on
     RH001 2026-09-02 21:33Z with the screensaver actually on screen and the
     SERVED css verified to contain `will-change` and `translate3d`: still
     96.4% of a core. Do not try it again without reading the next paragraph.

     ⭐ WHY IT COULD NEVER HAVE WORKED, and it is a bigger fact than this
     element: **the WebKit render process on this box has NO GPU device open at
     all** — zero handles on a DRI render node, on a machine with an RTX 5090
     in it. Every layer is composited on the CPU. "Promote it to its own
     compositing layer" is advice that assumes a compositor with a GPU under
     it; here it just relabels which CPU loop does the repainting. The correct
     fix for the whole interface is to get the card actually driving it, and
     that is boot-chain work, not a CSS property.

     ⭐ SO THIS ANIMATION STOPS BEING CONTINUOUS. Founder ruled STEPS,
     2026-09-02: the mark HOPS between positions instead of gliding, which
     costs one repaint per step instead of one per frame — from ~60/s to
     ~0.26/s, a factor of about 230.

     ⚠️ AND THE FUNCTION IS UNHARMED, WHICH IS THE ONLY REASON THIS IS
     ACCEPTABLE. The entire purpose is that no pixel stays lit — burn-in is
     caused by pixels not moving over MINUTES, and nothing about it needs the
     motion to be smooth. A mark that visits twelve positions across 47s
     protects the panel exactly as well as one that slides through them.
     What is genuinely lost is the founder's heartbeat, and it is lost
     deliberately and reversibly: restore `ss-breathe` the day the compositor
     has a GPU behind it. */
  animation: ss-step 47s steps(1, end) infinite;
}

/* ⛔ ss-breathe AND ss-drift ARE GONE, REPLACED BY ONE STEPPED ANIMATION.
   Two continuous animations meant two independent streams of repaints on a
   CPU-composited masked element. One stepped animation means one repaint per
   step, and every step moves BOTH the position and the tint, so the mark still
   changes in two ways without either change costing a frame loop.

   Twelve stops across 47 seconds: a move roughly every four seconds. The
   period is deliberately not a round number so the stops never phase-lock with
   anything else on the machine into a fixed pattern of lit pixels — the same
   reasoning the old 47s drift carried, kept.

   The opacity values are the old heartbeat's, sampled: it still sits mostly
   dim and comes up bright now and then, so the mark is READ rather than
   glimpsed. It reads as a slow blink rather than a breath. That is the trade
   the founder ruled and it is written down so nobody "improves" it back into a
   continuous animation without knowing what it costs. */
@keyframes ss-step {
   0%     { transform: translate(-3.2vw, -2.4vh); opacity: .24; }
   8.33%  { transform: translate(-1.1vw, -2.8vh); opacity: .95; }
  16.66%  { transform: translate( 1.4vw, -2.1vh); opacity: .88; }
  25%     { transform: translate( 3.2vw, -1.6vh); opacity: .34; }
  33.33%  { transform: translate( 3.0vw,  0.4vh); opacity: .66; }
  41.66%  { transform: translate( 2.7vw,  1.9vh); opacity: .24; }
  50%     { transform: translate( 2.4vw,  2.8vh); opacity: .90; }
  58.33%  { transform: translate( 0.6vw,  2.6vh); opacity: .40; }
  66.66%  { transform: translate(-1.3vw,  2.4vh); opacity: .72; }
  75%     { transform: translate(-2.8vw,  1.8vh); opacity: .26; }
  83.33%  { transform: translate(-3.1vw,  0.3vh); opacity: .84; }
  91.66%  { transform: translate(-3.4vw, -1.0vh); opacity: .32; }
 100%     { transform: translate(-3.2vw, -2.4vh); opacity: .24; }
}

/* Someone who asked for less motion gets the burn-in protection without the
   animation: the drift stays (it is the functional part), the breath goes. */
@media (prefers-reduced-motion: reduce) {
  /* The burn-in protection is the functional half and must survive a motion
     preference; the tint changes are the decorative half and need not. Now
     that the animation is stepped, "reduced motion" is nearly what everyone
     gets anyway — this keeps the movement and pins the opacity. */
  .ss-mark {
    animation: ss-step 47s steps(1, end) infinite;
    opacity: 0.28;
  }
}

/* ============================== kits ============================== */
/* The Kit card. Reuses .card, .band, .cards and .tag from above — a Kit is not
   a new visual language, it is another thing the box has. */
.kit-card { padding: 20px 22px 22px; }
.kit-head { display: flex; gap: 14px; align-items: flex-start; }
.kit-mark { flex: 0 0 auto; margin-top: 2px; opacity: .85; }
.kit-id { min-width: 0; }
.kit-id b { font-weight: var(--w-medium); font-size: var(--t-body); margin-right: 8px; }
.kit-sub { font-size: var(--t-small); color: var(--ink-40); margin-top: 3px; }
.kit-summary { margin: 14px 0 0; font-size: var(--t-small); color: var(--ink-60); }
.kit-facts { margin: 8px 0 0; font-size: var(--t-small); color: var(--ink-40); }
/* ⛔ The honesty line is not styled as a warning. It is not a fault — it is the
   privilege model working, and an amber panel would teach the owner to read a
   correct machine as a broken one. Quiet, and on the card, where the claim it
   qualifies actually is. */
.kit-note {
  margin: 12px 0 0; padding: 10px 12px; font-size: var(--t-small); line-height: 1.5;
  color: var(--ink-60); background: var(--paper-deep);
  border-left: 2px solid var(--alu-soft); border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
@media (max-width: 480px) {
  .kit-card { padding: 16px; }
  .kit-head { gap: 10px; }
}

/* ⚠️ FOUND BY RENDERING, NOT BY READING. The shipped Kits screen has used
   `class="lede"` since it went on the box and this stylesheet has never had a
   rule for it — so the standfirst under "Kits." rendered at body size in full
   ink, indistinguishable from a paragraph. Defined here at the same weight as
   .sub, which is what it was clearly meant to be. */
.lede { margin-top: 10px; font-size: var(--t-lead); color: var(--ink-60); max-width: 62ch; }

.backlink {
  display: inline-block; margin-bottom: 18px; font-size: var(--t-small);
  color: var(--ink-40); text-decoration: none;
}
.backlink:hover { color: var(--ink); }
.kit-acts { margin-top: 16px; }

/* ---------- how a Kit thinks, said on the card before anything is started --- */
.kit-brain { margin: 12px 0 0; font-size: var(--t-small); line-height: 1.5; }
.kit-brain-local   { color: var(--local); }
.kit-brain-offbox  { color: var(--oxford); }
.kit-brain-unknown { color: var(--amber); }

/* ============================ THE BRAIN BADGE ============================
   ⛔ THE HIGHEST-STAKES ELEMENT IN THIS STYLESHEET.
   The commercial premise of this box is that "AI processing · Local" is TRUE.
   So this component has exactly three appearances and they must be
   tellable apart across a desk, in a photograph, and by someone who cannot
   distinguish the colours at all — which is why the headline is a WORD
   ("On this box" / "Not on this box" / "Not recorded") and the colour only
   agrees with it.

   Colour allocation, and why:
     GREEN  --local  is the ONLY green, and it is spent only on an observed
                     local run. Nothing else here may ever be green.
     OXFORD --oxford is off-box. It is the brand accent, 9.23:1 on every
                     ground, and it is deliberately NOT red: thinking through
                     a service the owner connected is legitimate (ADR-0010),
                     it is simply not local, and dressing it as a fault would
                     teach the owner to distrust a correct machine.
     AMBER  --amber  is not-recorded, reusing this product's existing word for
                     "this check did not run" (js/ui.js). Never green.
   No new hue is introduced. Grounds are existing tokens only.               */
.brain {
  display: flex; gap: 16px; align-items: flex-start;
  margin: 26px 0 0; padding: 18px 20px;
  border: 1px solid var(--alu-soft); border-left-width: 3px;
  border-radius: var(--r-md); background: var(--paper-raise);
  max-width: 760px;
}
.brain-mark {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; font-size: var(--t-small); line-height: 1;
  border: 1px solid currentColor;
}
.brain-label {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
}
.brain-head { display: block; font-weight: var(--w-medium); font-size: var(--t-lead); margin-top: 4px; }
.brain p { margin-top: 8px; font-size: var(--t-small); line-height: 1.55; color: var(--ink-60); max-width: 60ch; }
.brain .brain-model { color: var(--ink-40); font-size: var(--t-caption); }
.brain-local  { border-left-color: var(--local);  background: var(--local-tint); }
.brain-local .brain-mark, .brain-local .brain-head { color: var(--local); }
.brain-offbox { border-left-color: var(--oxford); background: var(--paper-deep); }
.brain-offbox .brain-mark, .brain-offbox .brain-head { color: var(--oxford); }
.brain-unknown { border-left-color: var(--amber); background: var(--amber-tint); }
.brain-unknown .brain-mark, .brain-unknown .brain-head { color: var(--amber); }

/* The topbar pill's fourth appearance: a run that thought off this box. Same
   oxford, same reasoning. There is no green here and there never can be. */
.pill.offbox { color: var(--oxford); border-color: color-mix(in srgb, var(--info) 30%, transparent); background: var(--paper-deep); }
.dot.offbox  { background: var(--oxford); }

/* ⚠️ NARROW-SCREEN SAFETY. Rendered through a 375px frame, the run pill ran off
   the right edge of the topbar and took "off this box" with it — the half of
   the sentence that carries the whole meaning. The subject goes first, so the
   subject is what gets dropped. The answer is never truncated and never
   ellipsised: a privacy claim that has been cut in half is worse than one that
   is not shown at all. */
@media (max-width: 620px) {
  .pill-who { display: none; }
}
/* ⛔ THE CHROME IS WHAT OVERFLOWS THE PHONE, AND IT DOES IT TOO QUIETLY TO SEE.
   Measured through a 375px frame on 2026-09-15 (ui/frame375.html — headless
   Chrome clamps its viewport to 500px, so --window-size=375 measures the wrong
   breakpoint and says everything is fine). Against a 375px viewport, EVERY
   route ran the page's own horizontal scrollbar: #/apps 378, #/home 383,
   #/activity 391, #/system 392, #/computers 414. Between 3px and 39px — small
   enough that a screenshot hides it, and only documentElement.scrollWidth
   shows it. The same defect on every screen, because it is not a screen.

   .topbar is ONE flex line carrying three white-space:nowrap items — .crumb,
   .clock and the privacy pill — and not one of them had permission to give
   way, so the line's min-content width stood behind the 64px rail and pushed.
   The overflow differs per route only because the crumb is a word of a
   different length ("Home" vs "Computers"), which is the tell that the bar and
   not the content is the cause.

   THE PRIORITY ORDER, FIRST-TO-LOSE FIRST: the clock, then the breadcrumb,
   then the pill — and the pill is never lost. It is the product's permanent
   privacy indicator (project rule 3) and the one item in this bar that may not
   shrink, ellipsise or disappear at any width. These rules are sized against
   its LONGEST HONEST STATE, "AI processing · could not verify" at ~230px, not
   against the happy 169px "AI processing · Local" — the could-not-verify
   wording is the one that must survive a phone, because that is the reading an
   owner most needs to see and the one rule 4 forbids us to soften. */
.topbar-right { flex: 0 0 auto; }
/* Scoped to the bar, not to .pill everywhere: the same pill class is used inside
   .setrow on System, where it SHOULD still be free to shrink. A stream that can
   be kept to the chrome is kept to the chrome. */
.topbar-right .pill { flex: 0 0 auto; }
/* The crumb is the elastic member, and it was already nowrap for a good reason
   found earlier: it wrapped to three lines at 375px and pushed the clock and the
   pill into each other. Kept on one line — min-width:0 now lets it shrink past
   its own content, and the nowrap turns that into an ellipsis instead of a wrap.
   Never reached on a desktop. */
.crumb { white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

@media (max-width: 480px) {
  /* FIRST TO GO. A phone carries a clock in its own status bar a centimetre
     above this one, and this one is aria-hidden="true" — it is not in the
     accessibility tree, so hiding it removes nothing from anybody. */
  .clock { display: none; }
  /* SECOND TO GO — and display:none is right HERE where it is wrong in the
     rail. The rail's nav labels are CLIPPED rather than hidden because they are
     the only accessible name those icon-only links have (WCAG 2.1 AA 4.1.2 and
     2.4.4); see the 760px block above, and do not copy this line up there. The
     crumb is in a different position: it names the screen a SECOND time. The
     page's own <h1> says it immediately below, and the rail marks the same
     screen aria-current="page". Dropping a duplicate from the accessibility
     tree costs nothing; dropping a sole accessible name costs the link. */
  .crumb { display: none; }
  /* Which leaves the bar carrying exactly one item, whole and untruncated. */
  .topbar { padding: 12px 16px; }
}

/* ---------- giving a Kit a job ----------
   ⛔ A TEXTAREA AND A BUTTON. That is the whole instruction set. No command,
   no flag, no argument, nothing to look up. The Kit surface shows jobs and
   never a shell (SPEC-KITS-UX §1). */
.jobbox {
  margin-top: 26px; max-width: 760px; padding: 20px 22px 22px;
  border: 1px solid var(--alu); border-radius: var(--r-lg);
  background: var(--paper-raise);
}
.jobbox-label { display: block; font-size: var(--t-lead); margin-bottom: 12px; }
.jobbox-in {
  width: 100%; resize: vertical; font: inherit; font-size: var(--t-body); line-height: 1.55;
  color: var(--ink); background: var(--paper); border: 1px solid var(--alu);
  border-radius: var(--r-md); padding: 14px 16px;
}
.jobbox-in::placeholder { color: var(--ink-40); }
.jobbox-in:focus { outline: none; border-color: var(--ink); }
.jobbox-in:disabled { background: var(--paper-deep); color: var(--ink-40); }
.jobbox-foot { display: flex; align-items: center; gap: 14px; margin-top: 14px; flex-wrap: wrap; }
.jobbox-hint { font-size: var(--t-small); color: var(--ink-40); }
.jobbox-hint.bad { color: var(--red); }
.jobbox .chips { margin-top: 16px; }
.jobbox .chip { text-align: left; }
.jobbox .notice { margin-top: 16px; }

/* ---------- one run ---------- */
.run-page .backlink { margin-bottom: 22px; }
.run-head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.run-status {
  display: inline-flex; align-items: center; gap: 10px;
  font-weight: var(--w-display); font-size: var(--t-title-1); letter-spacing: -.015em;
}
/* ⛔ "Finished" IS NOT GREEN, AND THAT IS DELIBERATE.
   It was, in the first render, and it sat directly above a brain badge saying
   the run had thought off this box. Two greens on one screen, one of them
   meaning "the job completed" and the other meaning "nothing left the
   machine", and a glance cannot tell them apart. On this product green has
   exactly one job — it is spent on the privacy claim and on nothing else. A
   finished run is stated in ink; the word does all the work. */
.run-status.run-busy { color: var(--ink); }
.run-status.run-done { color: var(--ink); }
.run-status.run-stop { color: var(--red); }
.run-when { font-size: var(--t-small); color: var(--ink-40); font-variant-numeric: tabular-nums; }
.spinner.sm { width: 18px; height: 18px; margin: 0; border-width: 2px; }
/* The owner's own words, given the weight the owner's own words get elsewhere
   in this product (.msg-q on Ask). This is the thing they asked for. */
.run-job {
  margin-top: 16px; max-width: 760px; padding: 16px 20px;
  background: var(--paper-deep); border-radius: var(--r-lg);
  font-size: var(--t-lead); line-height: 1.5;
}
.run-h2 { margin: 34px 0 0; font-weight: var(--w-regular); font-size: var(--t-title-3); letter-spacing: -.01em; }
/* ⚠️ .state-panel is a CENTRED 48px-tall panel built for an empty screen. Used
   for a run's failure it left a large red void under two lines of text, which
   reads as alarm out of proportion to a folder that has not been shared yet.
   Tightened and left-aligned; the colour still does the work. */
.run-error { max-width: 760px; text-align: left; margin: 26px 0 0; padding: 22px 24px 24px; }
.run-error h2 { font-size: var(--t-title-3); }
.run-error p { margin: 0; max-width: 60ch; }
.run-error .actions { margin-top: 16px; }
.run-foot { margin-top: 18px; font-size: var(--t-small); color: var(--ink-40); max-width: 60ch; }

/* ⛔ THE TRANSCRIPT IS NOT A TERMINAL. Paper ground, the product's own face,
   sentences — not mono, not dark, not a scroll-locked box with a caret. If a
   line ever arrives that needs a monospace font to be readable, the defect is
   in what the Kit emitted, and it is fixed there. */
.runlog { list-style: none; margin-top: 16px; max-width: 760px; }
.runline {
  display: flex; gap: 18px; align-items: baseline;
  padding: 12px 0; border-bottom: 1px solid var(--alu-soft);
  font-size: var(--t-body); line-height: 1.55;
}
.runline:last-child { border-bottom: none; }
.runline time {
  flex: 0 0 auto; width: 48px; color: var(--ink-40); font-size: var(--t-caption);
  font-variant-numeric: tabular-nums;
}
.runline span { min-width: 0; }
.runline.pending span { color: var(--ink-40); }
.runline.pending span::after {
  content: ""; display: inline-block; width: 1px; height: .95em;
  margin-left: 3px; background: var(--ink-40); vertical-align: -1px;
  animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .runline.pending span::after { animation: none; } }

/* ---------- what the run left behind ---------- */
.outfiles { margin-top: 16px; max-width: 760px; border: 1px solid var(--alu-soft); border-radius: var(--r-md); overflow: hidden; }
.outfile {
  display: flex; align-items: center; gap: 14px; padding: 14px 18px;
  background: var(--paper-raise); border-bottom: 1px solid var(--alu-soft);
}
.outfile:last-child { border-bottom: none; }
/* ⚠️ There was a small outlined rectangle here as a document glyph. Rendered,
   it read as an UNCHECKED CHECKBOX sitting beside every file the run produced
   — an owner would reasonably try to tick it. Removed rather than redrawn: the
   filename is the strongest thing on the row already. */
.outfile-t { flex: 1; min-width: 0; }
.outfile-t b { display: block; font-weight: var(--w-medium); font-size: var(--t-body); }
.outfile-t span { display: block; font-size: var(--t-caption); color: var(--ink-40); overflow-wrap: anywhere; }
.outfile-s { font-size: var(--t-caption); color: var(--ink-40); font-variant-numeric: tabular-nums; }
.runrow { text-decoration: none; color: inherit; }
.runrow:hover { background: var(--paper-deep); }

@media (max-width: 760px) {
  .brain, .jobbox, .runlog, .outfiles, .run-job { max-width: none; }
  .run-status { font-size: var(--t-title-2); }
}
@media (max-width: 480px) {
  .brain { flex-direction: column; gap: 10px; padding: 16px; }
  .jobbox { padding: 16px; }
  .outfile { flex-wrap: wrap; }
  .outfile-t { flex: 1 0 70%; }
}

/* ==========================================================================
   THE KIT STAGE — what a Kit looks like when it is the thing the box is FOR
   ==========================================================================
   ⛔ WHY THE COUNTER BAND CAME OFF THIS SCREEN.
   Kits opened with three statistic cells — INSTALLED 1 / NOT CHECKED FROM
   HERE 1 / UNREADABLE RECORDS 0 — over a list containing one Kit. Rendered at
   3840×2160 and looked at, that is the most placeholder-like element in the
   product: a dashboard reporting on itself, three numbers where one noun
   would do, and the actual subject of the page pushed below the fold of the
   eye into a 320px card. The two facts those cells carried are TRUE and are
   kept — they are now said in words, on the Kit itself, where they are about
   something. A count is what goes on a screen when nobody has decided what
   the screen is for. */
.kitstage {
  margin-top: 30px;
  border: 1px solid var(--alu); border-radius: var(--r-lg);
  background: var(--paper-raise); overflow: hidden;
}
.kitstage-top {
  display: flex; align-items: flex-start; gap: 26px;
  padding: 30px 34px 26px;
}
.kitstage-mark { flex: 0 0 auto; opacity: .9; margin-top: 4px; }
.kitstage-id { min-width: 0; flex: 1; }
/* ⚠️ The tag used to be the flex child AFTER a `flex: 1` block, which parked
   it against the right edge of the panel — on a 4K render that is 900px from
   the word it qualifies, and a status that far from its subject qualifies
   nothing. It now sits on the name's own baseline. */
.kitstage-nameline { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.kitstage-name {
  font-weight: var(--w-display); letter-spacing: -.015em; line-height: 1.05;
  font-size: var(--t-title-2);
}
.kitstage-by { margin-top: 10px; font-size: var(--t-small); color: var(--ink-40); }
/* The Kit's own sentence, set as the promise it is rather than as body copy.
   These are the founder's words for what a Kit is; this screen exists to
   carry them, so they are the largest thing on it after the name. */
.kitstage-say {
  margin: 0; padding: 0 34px 30px;
  font-weight: var(--w-display); font-size: var(--t-title-3); line-height: 1.45;
  color: var(--ink); max-width: 62ch;
}
/* The facts strip: hairline-ruled like the Home check grid, deliberately NOT
   floating cards, and every cell is a sentence about this Kit. */
.kitfacts {
  display: grid; grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--alu-soft);
}
.kitfact {
  padding: 18px 34px 20px; border-right: 1px solid var(--alu-soft);
  display: flex; flex-direction: column; gap: 6px; min-width: 0;
}
.kitfact:last-child { border-right: none; }
.kitfact-label {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
}
.kitfact-val {
  font-weight: var(--w-display); font-size: var(--t-title-3); letter-spacing: -.01em;
  line-height: 1.2;
}
.kitfact-note { font-size: var(--t-small); color: var(--ink-40); line-height: 1.45; }
.kitfact.f-local   .kitfact-val { color: var(--local); }
.kitfact.f-offbox  .kitfact-val { color: var(--oxford); }
.kitfact.f-unknown .kitfact-val { color: var(--amber); }
.kitstage-foot {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 22px 34px 26px; border-top: 1px solid var(--alu-soft);
  background: var(--paper-deep);
}
.kitstage-foot .btn { padding: 14px 26px; font-size: var(--t-body); }
.kitstage-foot .hint { font-size: var(--t-small); color: var(--ink-40); max-width: 52ch; }
.kitstage + .kitstage { margin-top: 24px; }

/* The payoff strip on the list: what this box has already left the owner.
   "It leaves you the files" is half the founder's sentence for what a Kit is,
   so the Kits screen ends on files rather than on a Kit. */
.payoff { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; }
.payoff-file {
  display: flex; align-items: baseline; gap: 12px;
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  background: var(--paper-raise); padding: 12px 18px;
  text-decoration: none; color: inherit; max-width: 100%; min-width: 0;
}
.payoff-file:hover { background: var(--paper-deep); }
.payoff-file b {
  font-weight: var(--w-medium); font-size: var(--t-body); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.payoff-file span { font-size: var(--t-small); color: var(--ink-40); flex: 0 0 auto; }
/* ⚠️ .notice carries no margin-top by design (it normally opens a section), so
   after the payoff strip it welded to the chips with no gap. Rendered, 4K. */
.payoff + .notice, .rows + .notice { margin-top: 28px; }

/* ---------- the Kit page: composer leads, the honesty sits beside it ------
   ⛔ DOM ORDER IS brain -> composer -> aside AND MUST STAY THAT WAY.
   The grid puts the composer on the left at wide widths, but the brain badge
   still precedes it in the document, so a screen reader — and every width
   below 1700px — meets "where this Kit thinks" BEFORE the button that sends
   the owner's words. Telling them afterwards is telling them too late. */
.kitgrid {
  display: grid; gap: 26px; margin-top: 26px;
  /* minmax(0, 1fr), never the implicit auto column: an auto column is as wide
     as its widest nowrap chip, and on a 390 px phone that made the Kit page
     842 px wide (the phone zoomed out). Found by tests/ui/touch-targets. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "brain" "composer" "aside";
}
.kitgrid > .brain    { grid-area: brain;    margin-top: 0; max-width: none; }
.kitgrid > .jobbox   { grid-area: composer; margin-top: 0; max-width: none; }
.kitgrid > .kitaside { grid-area: aside; }

.kitaside {
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  background: var(--paper-raise); padding: 20px 22px 22px;
}
.kitaside h3 {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40); margin-bottom: 16px;
}
.kitaside ol { list-style: none; counter-reset: k; display: grid; gap: 18px; }
/* ⛔ EXACTLY TWO CHILDREN PER ROW, AND THE MARKUP MUST KEEP IT THAT WAY.
   `grid-template-columns: 24px 1fr` places ::before in column 1 and one
   element in column 2. Any THIRD grid item — including a bare text node left
   loose after </b> — wraps to the next row's 24px column and renders the
   sentence one word per line. It did. See the note in kits.js aside(). */
.kitaside li {
  counter-increment: k; display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: 14px; font-size: var(--t-small); color: var(--ink-60); line-height: 1.55;
}
.kitaside li::before {
  content: counter(k); font-variant-numeric: tabular-nums;
  color: var(--ink-40); font-size: var(--t-caption);
  border: 1px solid var(--alu-soft); border-radius: 50%;
  width: 24px; height: 24px; display: grid; place-items: center;
  align-self: start;
}
.kitaside li b {
  color: var(--ink); font-weight: var(--w-medium); display: block; margin-bottom: 4px;
  font-size: var(--t-body);
}
.kitaside li > div { min-width: 0; }
.kitaside li span { display: block; }

/* ---------- a run in progress: the newest sentence, large ----------------
   ⛔ NO PROGRESS BAR AND NO PERCENTAGE ON THIS SCREEN, EVER. Nothing in the
   run contract reports how far through a job is, so a bar would be an
   invented number rendered as a measurement — precisely the defect CLAUDE.md
   rule 4 names. What CAN be shown truthfully is the sentence the team said
   most recently and how long it has been going, and those happen to be the
   two things a person standing at the box actually wants. */
.run-now {
  margin-top: 22px; border: 1px solid var(--alu); border-radius: var(--r-lg);
  background: var(--paper-raise); padding: 26px 30px 28px;
  display: grid; gap: 12px;
}
.run-now-label {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
  display: flex; align-items: center; gap: 10px;
}
.run-now-line {
  font-weight: var(--w-display); font-size: var(--t-title-2); line-height: 1.35;
  letter-spacing: -.01em; max-width: 42ch;
}
.run-now-meta {
  font-size: var(--t-small); color: var(--ink-40);
  font-variant-numeric: tabular-nums;
}
/* A quiet breathing dot. It is decoration for a state the words already
   carry, so reduced-motion switches it off and nothing is lost.
   ⛔ INK, NOT GREEN, AND THIS ONE WAS CAUGHT IN A SCREENSHOT. The first
   version used --local because a live thing wants a live colour. Rendered
   against a CLOUD run it put a small green pulsing dot at the top of a screen
   whose whole subject is that the thinking is leaving the machine. Green in
   this product means "checked, and it happened here". It cannot also mean
   "busy". */
.run-pulse {
  width: 9px; height: 9px; border-radius: 50%; background: var(--ink-40);
  flex: 0 0 auto;
}
@media (prefers-reduced-motion: no-preference) {
  .run-pulse { animation: runpulse 1.9s ease-in-out infinite; }
}
@keyframes runpulse { 0%, 100% { opacity: 1; } 50% { opacity: .22; } }

/* ---------- the payoff: the moment a run leaves you files ----------------
   ⛔ THIS PANEL IS INK, NOT GREEN, AND THE RESTRAINT IS THE POINT.
   Green was the obvious choice — this is the good news on the screen. But in
   this product green means exactly one thing, "a check ran and the thinking
   happened on this box", and it is the signal the entire commercial premise
   rests on. Spending it a second time on "here are your files" would teach an
   owner that green means pleasant, and the day a cloud run sits next to a
   green panel that habit is what they read it with. So the payoff is set in
   the product's own emphasis colour — the same black as the button — and
   green stays a measurement. */
.outbox {
  margin-top: 26px; border: 1px solid var(--ink);
  border-radius: var(--r-lg); background: var(--paper-raise);
  overflow: hidden;
}
.outbox-head { padding: 24px 28px 0; }
.outbox-head b {
  display: block; font-weight: var(--w-display); letter-spacing: -.01em;
  font-size: var(--t-title-2); color: var(--ink); line-height: 1.2;
}
.outbox-head span {
  display: block; margin-top: 8px; font-size: var(--t-small); color: var(--ink-60);
  max-width: 46ch; line-height: 1.5;
}
.outbox .outfiles {
  margin: 18px 20px 20px; max-width: none;
  border-color: var(--alu-soft); background: var(--paper);
}
.outbox .outfile { background: var(--paper); }

/* ---------- the run page grid ------------------------------------------- */
/* Stacked, the order is source order: brain, files, then the account of what
   happened. The named areas exist so the ≥1700px rule can put the transcript
   on the left without the markup having to lie about what comes first. */
.rungrid { display: grid; gap: 26px; grid-template-areas: "side" "main"; }
.rungrid > .run-main { grid-area: main; min-width: 0; }
.rungrid > .run-side { grid-area: side; min-width: 0; }
.rungrid .brain, .rungrid .outbox, .rungrid .runlog, .rungrid .outfiles { max-width: none; }
.rungrid .brain { margin-top: 0; }
.rungrid .outbox { margin-top: 0; }
.rungrid .run-side > * + * { margin-top: 22px; }

/* ==========================================================================
   WIDE GLASS — the box is a 4K panel on a desk, not a laptop lid
   ==========================================================================
   ⛔ FOUND BY RENDERING AT 3840×2160 AND LOOKING AT IT, 2026-09-03, after the
   founder walked up to the reference box at 2am and read finished work as a
   placeholder. `.page` was capped at 1080px and never centred, so on that
   panel every screen in the product was a narrow column of 15px type pinned
   to the top-left corner with about three quarters of the glass empty.
   Nothing was broken. It just looked unfinished — which, on something sold as
   an appliance, is the same thing.

   ⭐ THIS TIER IS PURELY ADDITIVE AND STARTS AT 1700px. Every width the
   product has previously been rendered and signed off at — 1920, 1280, 1024,
   760, 500, 375 — is unchanged, because nothing below 1700 is touched.

   The width is spent on STRUCTURE, not on line length: reading measures stay
   in ch, the type grows because the viewing distance grew, and the Kit
   surface uses the second column it now has. A 3000px line of 15px text would
   have been a worse screen than the one being fixed. */
@media (min-width: 1700px) {
  :root {
    --fs-micro: 12.5px;
    --fs-sm:    14.5px;
    --fs-base:  17.5px;
    --fs-lg:    21px;
    --fs-h2:    25px;
    --fs-h1:    64px;
    --fs-stat:  42px;
    --rail-w:   282px;
    --radius:    8px;
    --radius-lg: 13px;
  }
  .page { max-width: 1560px; padding: 52px 60px 110px; margin-inline: auto; }
  .brand img { width: 190px; }
  .nav a { font-size: var(--t-body); padding: 12px 13px; }
  .nav a svg { width: 19px; height: 19px; }
  .topbar { padding: 15px 28px; }
  .btn { font-size: var(--t-small); padding: 12px 22px; }
  .btn.sm { padding: 9px 16px; }
  .row { font-size: var(--t-body); padding: 17px 24px; }
  .row .t { width: 74px; }
  .row .k { width: 138px; }
  .check { min-height: 168px; padding: 24px 26px 26px; }
  .check-val { font-size: var(--t-title-1); }
  .check-name { font-size: var(--t-small); }
  .notice { font-size: var(--t-small); padding: 14px 20px; }
  .state-panel { max-width: 820px; padding: 60px 48px; }
  .state-panel p { font-size: var(--t-body); max-width: 56ch; }
  .sub, .lede { font-size: var(--t-lead); }
  /* ⚠️ These were 13–14.5px literals, so on System — the densest screen in the
     product — the headings grew and every sentence under them stayed at
     laptop size. Rendered at 3840×2400 and it read as two products stacked. */
  .stat { padding: 22px 26px; }
  .stat .foot { font-size: var(--t-small); }
  .stat .value.words { font-size: var(--t-title-3); }
  .setrow { padding: 20px 24px; }
  .setrow .info > b { font-size: var(--t-body); }
  .setrow .info span { font-size: var(--t-small); }
  .setrow .val { font-size: var(--t-small); }
  table { font-size: var(--t-body); }
  .sect .more { font-size: var(--t-small); }
  .day { font-size: var(--t-caption); padding: 11px 24px; }

  /* Kit surface */
  .kit-summary { font-size: var(--t-body); }
  .kit-facts, .kit-brain, .kit-note { font-size: var(--t-small); }
  .brain { max-width: none; padding: 24px 28px; }
  .brain p { font-size: var(--t-small); }
  .brain .brain-model { font-size: var(--t-caption); }
  /* ⚠️ THE RING HAS TO GROW WITH THE GLYPH. Scaling only the font-size drove a
     26px mark's character out through a 26px circle, and the offbox arrow
     rendered as a slash across a disc — it read as a "no entry" sign on the
     panel that says a Kit is ALLOWED to think elsewhere. Photographed at 4K
     before the fix; the exact inverse of the meaning. */
  .brain-mark { width: 34px; height: 34px; font-size: var(--t-body); }
  .jobbox { max-width: none; padding: 30px 32px 32px; }
  .jobbox-in { font-size: var(--t-lead); padding: 18px 20px; }
  .jobbox-hint { font-size: var(--t-small); }
  .chip { font-size: var(--t-small); padding: 10px 16px; }
  .run-job { max-width: none; font-size: var(--t-lead); padding: 20px 24px; }
  .run-status { font-size: var(--t-display); }
  /* The stopped-run panel kept a 760px cap while everything round it grew, so
     the one thing on the screen that had gone wrong was the smallest object
     on it. Measured at 3840×2160. */
  .run-error { max-width: 1100px; padding: 30px 34px 32px; }
  .run-error h2 { font-size: var(--t-title-3); }
  .run-error p { font-size: var(--t-body); }
  .runlog { max-width: none; }
  .runline { font-size: var(--t-body); padding: 15px 0; }
  .runline time { width: 80px; font-size: var(--t-small); }
  .run-foot { font-size: var(--t-small); }
  .outfiles { max-width: none; }
  .outfile { padding: 18px 22px; }
  .outfile-t b { font-size: var(--t-lead); }
  .outfile-t span { font-size: var(--t-small); }
  .outfile-s { font-size: var(--t-small); }
  .kitstage-top { padding: 40px 44px 32px; gap: 28px; }
  .kitstage-say { padding: 0 44px 34px; }
  .kitfact { padding: 24px 44px 26px; }
  .kitstage-foot { padding: 28px 44px 32px; }
  .kitstage-foot .btn { padding: 17px 34px; font-size: var(--t-lead); }

  /* The second column, which is the whole reason the glass is this wide. */
  .kitgrid {
    grid-template-columns: minmax(0, 1.45fr) minmax(380px, 1fr);
    grid-template-areas: "composer brain" "composer aside";
    align-items: start; gap: 32px;
  }
  .rungrid {
    grid-template-columns: minmax(0, 1.5fr) minmax(420px, 1fr);
    grid-template-areas: "main side"; align-items: start; gap: 44px;
  }
}

@media (min-width: 2400px) {
  :root {
    --fs-micro: 15px;
    --fs-sm:    17px;
    --fs-base:  21px;
    --fs-lg:    26px;
    --fs-h2:    31px;
    --fs-h1:    88px;
    --fs-stat:  56px;
    --rail-w:   340px;
  }
  .page { max-width: 2160px; padding: 76px 80px 140px; }
  .brand img { width: 236px; }
  .nav a { padding: 15px 15px; }
  .nav a svg { width: 23px; height: 23px; }
  .check-val { font-size: var(--t-display); }
  .run-status { font-size: var(--t-display); }
  .brain-mark { width: 40px; height: 40px; font-size: var(--t-title-3); }
  .kitstage-top { padding: 54px 58px 40px; }
  .kitstage-say { padding: 0 58px 44px; }
  .kitfact { padding: 30px 58px 32px; }
  .kitstage-foot { padding: 36px 58px 40px; }
  .outfile { padding: 22px 26px; }
  .runline { padding: 19px 0; }
  .runline time { width: 100px; }
  .row .k { width: 160px; }
}

/* The chip row gets a label because its contents are no longer generic
   suggestions — they are the owner's own previous asks, and an unlabelled
   row of them reads as "things the machine wants you to press" rather than
   "things you already asked it". Same component, different provenance, and
   the provenance is the whole point. */
.chiprow { margin-top: 20px; }
.chiprow-label {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
}
.chiprow .chips { margin-top: 10px; }
.jobbox .chiprow .chip { text-align: left; white-space: normal; }

.run-next { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 34px; }
@media (min-width: 1700px) {
  .run-next { margin-top: 44px; }
  .run-next .btn { padding: 16px 30px; font-size: var(--t-body); }
  .jobbox-foot .btn { padding: 16px 32px; font-size: var(--t-body); }
  .run-now { padding: 34px 38px 36px; }
  .backlink { font-size: var(--t-small); }
}

/* ==========================================================================
   THE KIT SURFACE ON A PHONE
   ==========================================================================
   ⚠️ FOUND BY RENDERING THROUGH ui/frame375.html, 2026-09-03. The facts strip
   is a three-column grid and it had no narrow rule, so at 375px each cell was
   about 65 CSS pixels wide and every sentence in it set one word per line for
   several screens. This is the SECOND time tonight the same class of mistake
   showed up — a grid whose columns were reasoned about at one width only —
   and both were invisible until something rendered them. Nothing here touches
   a class that existed before this stream. */
@media (max-width: 760px) {
  .kitfacts { grid-template-columns: 1fr; }
  .kitfact {
    border-right: none; border-bottom: 1px solid var(--alu-soft);
    padding: 16px 20px 18px;
  }
  .kitfact:last-child { border-bottom: none; }
  .kitstage-top { padding: 22px 20px 18px; gap: 16px; }
  .kitstage-say { padding: 0 20px 22px; font-size: var(--t-lead); max-width: none; }
  .kitstage-foot { padding: 18px 20px 20px; }
  .kitaside { padding: 18px 18px 20px; }
  .outbox-head { padding: 20px 18px 0; }
  .outbox-head span { max-width: none; }
  .outbox .outfiles { margin: 14px 14px 16px; }
  .run-now { padding: 20px 18px 22px; }
  .run-now-line { max-width: none; }
  .payoff-file { width: 100%; }
  .run-next .btn { flex: 1 0 auto; }

  /* ⚠️ SCOPED TO .runrow ON PURPOSE. SPEC-KITS-UX §6.2 already names the fact
     that `.row` keeps its three columns on one flex line at phone widths and
     collapses the description to three words a line. That defect belongs to
     Activity's owner and is not being smuggled into a fix here — but the Kit
     surface now puts run rows on two more screens, and propagating a known
     defect to new screens because it is somebody else's is not a defence.
     `.runrow` is this stream's own class, so it is fixed here and Activity's
     rows are byte-identical to before. */
  .runrow { flex-wrap: wrap; row-gap: 6px; }
  .runrow .t { width: auto; }
  .runrow .k { width: auto; }
  .runrow .what { flex: 1 0 100%; }
}
@media (max-width: 480px) {
  .kitstage-name { font-size: var(--t-title-1); }
  .kitstage-mark { width: 44px; height: 44px; }
  .kitstage-foot { flex-direction: column; align-items: stretch; gap: 12px; }
  .kitstage-foot .btn { justify-content: center; }
}

/* ── Apps ───────────────────────────────────────────────────────────────────
   Software the owner runs ON the box. The app opens INSIDE this screen, framed,
   because the kiosk has no back button — see ui/js/screens/apps.js.

   ⭐ THE TILE, REDESIGNED 2026-10-03 (founder, 22:46: "THE APP TILES IN GENERAL
   COULD BE MORE SPECIAL ACROSS THE NETWORK AND BOXES"). One shape for every app
   on the screen — built in, added, or held by another box of the fleet:
     · the app's OWN mark, large (72px; 80 from 1700; 96 in "Made for your
       home") on a soft shadow, sized so a 1024px icon simply drops in;
     · a faint wash of the app's own colour (manifest `accent`) falling from
       the top of the tile and gone before the words — neutral when it has none;
     · a state chip that carries its WORD, never colour alone;
     · "Open" as the one primary action, at a 44px target.
   ⛔ NO DARK THEME IS DRAWN HERE. OxOnOS has none; a dark Apps grid on a paper
   page would be the "dark-mode catastrophe" P0 class, not a feature. Every
   colour below is a token or a mix of one, so a theme that redefines the
   tokens carries the tiles with it. */
.apps-sect { margin-top: 56px; padding-top: 26px; border-top: 1px solid var(--alu-soft); }
.apps-sect--first { margin-top: 40px; }
.apps-sect__head { display: flex; flex-direction: column; gap: 6px; }
.apps-sect__title {
  font-weight: var(--w-display); font-size: var(--t-title-3); line-height: 1.1;
  letter-spacing: -.015em;
}
.apps-group {
  margin-top: 30px; font-size: var(--t-caption); font-weight: var(--w-medium);
  letter-spacing: normal; text-transform: none;
  color: var(--ink-40);
}
.apps-alone { margin-top: 40px; max-width: 60ch; }

.app-grid {
  /* min(…, 100%): a fixed floor wider than a phone column is a card the page
     scrolls sideways to reach. Measured through the 375 frame, 2026-09-30. */
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(290px, 100%), 1fr));
  gap: 16px; margin-top: 14px;
}
.app-grid--feature { grid-template-columns: repeat(auto-fill, minmax(min(400px, 100%), 1fr)); gap: 18px; }

.app-card {
  --app-accent: var(--oxford);
  --app-wash: 0%;
  position: relative;
  display: flex; flex-direction: column; align-items: stretch; min-width: 0;
  padding: 22px 22px 20px;
  border: 1px solid var(--alu-soft); border-radius: var(--r-lg);
  background: var(--paper-raise);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--app-accent) var(--app-wash), transparent) 0,
      transparent 128px),
    var(--paper-raise);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .03);
}
/* Only an app that NAMED a colour gets a wash; everything else stays paper. */
.app-card--tinted { --app-wash: 15%; }
.app-card--own { --app-wash: 6%; }
.app-card--own.app-card--tinted { --app-wash: 12%; }
.app-card--feature { --app-wash: 20%; padding: 26px 26px 24px; }
.app-card--feature.app-card:not(.app-card--tinted) { --app-wash: 8%; }

.app-card__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }

/* The mark: the app's own icon over its initial, so a mark that fails to load
   leaves a letter rather than a torn-page glyph or an empty square. */
.app-card__mark {
  position: relative; flex: 0 0 auto;
  width: 72px; height: 72px; border-radius: var(--r-icon); /* 10/42, the README's clip */
  overflow: hidden; display: grid; place-items: center;
  background: color-mix(in srgb, var(--app-accent) var(--app-wash), var(--paper-deep));
  box-shadow:
    0 1px 2px rgba(0, 0, 0, .10),
    0 10px 22px -10px color-mix(in srgb, var(--app-accent) 55%, rgba(0, 0, 0, .35));
}
.app-card--feature .app-card__mark { width: 96px; height: 96px; }
.app-card__mono {
  font-size: var(--t-title-1); font-weight: var(--w-display); line-height: 1; color: var(--ink);
  letter-spacing: -.02em;
}
.app-card--feature .app-card__mono { font-size: var(--t-display); }
/* Any icon, any size up to 1024px, fills the mark. Inline SVG marks too. */
.app-card__mark .app-card__icon,
.app-card__mark svg.app-card__icon {
  position: absolute; inset: 0; width: 100%; height: 100%; flex: none;
  object-fit: cover; border-radius: 0; display: block;
}
/* A real mark brings its own ground (the OxOn family and the third-party spec
   in ui/brand/apps/README.md both fill their square), so nothing of ours shows
   at its corners. The paper ground returns only for the monogram. */
.app-card__mark:has(.app-card__icon) { background: transparent; }

.app-card__name {
  margin-top: 16px; font-size: var(--t-title-3); font-weight: var(--w-bold); line-height: 1.2;
  letter-spacing: -.015em; color: var(--ink); overflow-wrap: anywhere;
}
.app-card--feature .app-card__name { font-size: var(--t-title-2); }
/* Height-matched to two lines, so buttons share a baseline across a row. */
.app-card__what {
  margin-top: 6px; color: var(--ink-60); font-size: var(--t-small); line-height: 1.5;
  min-height: calc(2 * 1.5 * 13.5px);
}
.app-card__state, .app-card__where {
  margin-top: 10px; font-size: var(--t-caption); line-height: 1.45; color: var(--ink-60);
}
.app-card__where { font-weight: var(--w-medium); color: var(--ink); }
.app-card__foot { margin-top: auto; padding-top: 18px; display: flex; }
.app-card__open {
  min-height: 44px; padding: 10px 24px; border-radius: var(--r-pill);
  font-size: var(--t-small); font-weight: var(--w-bold); letter-spacing: .01em;
}

/* State chips. ⛔ Colour never carries the state alone — every chip ships its
   word, and each pairing below is ≥ 4.5:1 (WCAG 1.4.3 / 1.4.1). */
.app-chip {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 1 auto; min-width: 0;
  padding: 5px 11px 5px 9px; border-radius: var(--r-pill);
  font-size: var(--t-caption); font-weight: var(--w-bold); letter-spacing: .02em; line-height: 1.3;
  border: 1px solid transparent; text-align: left;
}
.app-chip::before {
  content: ""; flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor;
}
.app-chip--ready { background: var(--local-tint); color: var(--local); }
.app-chip--down  { background: var(--amber-tint); color: var(--amber); }
.app-chip--quiet { background: var(--paper-deep); color: var(--ink-60); border-color: var(--alu-soft); }
.app-chip--quiet::before { background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }
.app-chip--away  { background: color-mix(in srgb, var(--oxford) 9%, var(--paper-raise)); color: var(--oxford); }

/* Installed but not answering, or held by a box that did not answer. Said
   plainly, never hidden and never green: the paper drops a tone and the mark
   loses its colour, but the WORDS keep full contrast. */
.app-card--down { background: var(--paper-deep); box-shadow: none; }
.app-card--down .app-card__mark { filter: grayscale(1); opacity: .62; box-shadow: none; }

/* Keyboard: the whole tile shows where focus is, not only the button. */
.app-card:has(:focus-visible) {
  outline: 2px solid var(--ink); outline-offset: 3px;
}
.app-card__open:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Lift on hover, press on click — only for people who have not asked the
   system to reduce motion, and only on a device that really hovers. */
@media (prefers-reduced-motion: no-preference) {
  .app-card { transition: transform .18s ease, box-shadow .18s ease; }
  .app-card__open { transition: transform .12s ease, background-color .12s ease; }
  .app-card__open:active { transform: scale(.97); }
  @media (hover: hover) {
    .app-card:not(.app-card--down):hover {
      transform: translateY(-3px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, .05), 0 18px 36px -18px rgba(0, 0, 0, .28);
    }
  }
}

@media (max-width: 480px) {
  .app-card { padding: 18px 18px 18px; }
  .app-card--feature .app-card__mark { width: 80px; height: 80px; }
  .app-card__open { flex: 1; }
  /* ⭐ ON A PHONE AN ORDINARY TILE IS A ROW, not a poster: mark on the left,
     chip, name and line beside it, sentence and button full width below.
     Stacked, eleven tiles measured ~4,500 CSS px of scrolling at 390. The
     featured tiles keep the poster shape — they are the ones meant to stop
     the eye. Same markup; .app-card__top dissolves into the grid. */
  .app-card:not(.app-card--feature) {
    display: grid; grid-template-columns: 56px minmax(0, 1fr);
    column-gap: 14px; align-items: start;
  }
  .app-card:not(.app-card--feature) .app-card__top { display: contents; }
  .app-card:not(.app-card--feature) .app-card__mark {
    grid-column: 1; grid-row: 1 / span 3; width: 56px; height: 56px;
  }
  .app-card:not(.app-card--feature) .app-card__mono { font-size: var(--t-title-2); }
  .app-card:not(.app-card--feature) .app-chip { grid-column: 2; justify-self: start; }
  .app-card:not(.app-card--feature) .app-card__name { grid-column: 2; margin-top: 8px; font-size: var(--t-lead); }
  .app-card:not(.app-card--feature) .app-card__what { grid-column: 2; min-height: 0; }
  .app-card:not(.app-card--feature) .app-card__state,
  .app-card:not(.app-card--feature) .app-card__where,
  .app-card:not(.app-card--feature) .app-card__foot { grid-column: 1 / -1; }
  .app-card:not(.app-card--feature) .app-card__foot { padding-top: 14px; }
}
.app-frame { display: flex; flex-direction: column; height: calc(100vh - 96px); }
.app-frame__bar {
  display: flex; align-items: center; gap: 14px; padding-bottom: 12px;
  border-bottom: 1px solid var(--alu-soft); margin-bottom: 12px;
}
.app-frame__name { font-size: var(--t-body); font-weight: var(--w-medium); }
.app-frame__note { margin-left: auto; font-size: var(--t-caption); color: var(--ink-40); }
.app-frame__view {
  flex: 1; width: 100%; border: 1px solid var(--alu-soft);
  border-radius: var(--r-md); background: var(--paper-raise);
}

/* The unpaired takeover — see showUnpaired() in ui/js/app.js. */
.pair-steps {
  text-align: left; margin: 18px auto 6px; padding-left: 22px;
  max-width: 46ch; color: var(--ink); font-size: var(--t-small); line-height: 1.7;
}
.pair-steps li { margin-bottom: 8px; }
.pair-steps a { color: var(--ink); }
.pair-note {
  font-size: var(--t-caption); color: var(--ink-40); max-width: 46ch;
  margin: 4px auto 18px;
}

/* ── OxOnTV ────────────────────────────────────────────────────────────────
   The face of the box's own television service. Built 2026-09-08 after the
   founder looked at the shipped product and asked "where is the life?" — the
   service had existed since that morning with no surface at all.

   Deliberately reuses the Home cell rhythm rather than inventing a second
   visual language for the same kind of fact. A number, a label, and a sentence
   underneath that says what it means for the person reading it. */
.tv-grid {
  /* min(280px, 100%): the band clips (overflow:hidden) — at 375 both cells
     lost their right 14px behind it. Measured 2026-09-30. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 0; margin-top: 16px;
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  overflow: hidden; background: var(--paper-raise);
}
.tv-cell { padding: 22px 24px 24px; border-right: 1px solid var(--alu-soft); }
.tv-cell:last-child { border-right: 0; }
.tv-cell__label {
  font-size: var(--t-caption); letter-spacing: normal; color: var(--ink-60);
  text-transform: none;
}
.tv-cell__value { font-size: var(--t-title-1); font-weight: var(--w-display); letter-spacing: -.02em; margin-top: 12px; }
/* ⛔ THE COLOUR IS EARNED. tv-unknown is NOT a warning and NOT a pass — the
   provider was not reachable, and could-not-ask is a third state that must not
   borrow either of the other two's colour (CLAUDE.md rule 4). */
.tv-cell__value.tv-warn { color: var(--amber, #8a6a1f); }
.tv-cell__value.tv-unknown { color: var(--ink-60); }
.tv-cell__foot { margin-top: 10px; font-size: var(--t-small); line-height: 1.5; color: var(--ink-60); }

.tv-provs { margin-top: 16px; display: grid; gap: 14px; }
.tv-prov {
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  background: var(--paper-raise); padding: 22px 24px 24px;
}
.tv-prov__head { display: flex; align-items: center; gap: 12px; }
.tv-prov__name { font-size: var(--t-lead); font-weight: var(--w-medium); letter-spacing: -.01em; }
.tv-prov__badge {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--paper-deep); color: var(--ink-60);
}
.tv-prov__badge--off { opacity: .75; }
.tv-prov__note { margin-top: 12px; font-size: var(--t-caption); line-height: 1.5; color: var(--ink-60); }

.tv-field { display: block; margin-top: 16px; }
.tv-field > span {
  display: block; font-size: var(--t-caption); letter-spacing: normal;
  color: var(--ink-60); margin-bottom: 6px;
}
.tv-field input {
  width: 100%; max-width: 520px; font: inherit; font-size: var(--t-body);
  padding: 11px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--alu-soft); background: var(--paper);
  color: inherit;
}
.tv-check { display: flex; align-items: center; gap: 9px; margin-top: 18px; font-size: var(--t-small); }
.tv-actions { margin-top: 18px; display: flex; align-items: center; gap: 14px; }
.tv-saved { font-size: var(--t-small); color: var(--ink-60); }
.tv-failed { font-size: var(--t-small); color: var(--red, #8c2f22); }

/* Not built yet, said in those words. The alternative was an empty channel
   grid, which reads as "your television is broken" rather than "this part is
   not written". Naming what is missing is the difference between an honest
   appliance and a demo. */
.tv-soon {
  margin-top: 16px; padding: 22px 24px 24px;
  border: 1px dashed var(--alu-soft); border-radius: var(--r-md);
  background: transparent;
}
.tv-soon__title { font-size: var(--t-body); font-weight: var(--w-medium); }
.tv-soon p { margin-top: 10px; font-size: var(--t-small); line-height: 1.6; color: var(--ink-60); max-width: 62ch; }
.tv-soon__foot { opacity: .85; }


/* ==========================================================================
   FILES — the owner's own documents (2026-09-16, stream B)
   ⛔ These classes exist because this screen stopped being a grants table and
   became the place an owner sees, opens and takes away their own files. Added
   as ONE block at the end so a parallel stream's edits to app.css do not
   collide with them.
   ========================================================================== */

/* Where you are, in folder names. ⛔ NEVER a Unix path — CLAUDE.md rule 2. */
.crumbs {
  margin: 20px 0 0; display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 6px; font-size: var(--t-small); color: var(--ink-60);
}
.crumbs a { color: var(--ink-60); text-decoration: none; border-bottom: 1px solid var(--alu-soft); }
.crumbs a:hover { color: var(--ink); border-bottom-color: var(--ink); }
.crumbs b { color: var(--ink); font-weight: var(--w-medium); }
.crumb-sep { color: var(--ink-40); }

/* The preview. One file, opened in place, closed by a button — not a modal,
   because a modal on a 4K screen standing in a room is a trap without a
   keyboard. */
.viewer {
  margin-top: 20px; border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); overflow: hidden;
}
.viewer-head {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 20px; border-bottom: 1px solid var(--alu-soft);
  background: var(--paper-deep);
}
.viewer-head b { flex: 1; min-width: 0; font-weight: var(--w-medium); overflow-wrap: anywhere; }
.viewer-head span { font-size: var(--t-small); color: var(--ink-40); font-variant-numeric: tabular-nums; }
.viewer-text {
  margin: 0; padding: 20px; max-height: 60vh; overflow: auto;
  white-space: pre-wrap; overflow-wrap: anywhere;
  font-size: var(--t-small); line-height: 1.55; color: var(--ink);
}
.viewer-img { display: block; max-width: 100%; max-height: 60vh; margin: 20px auto; }
.viewer-pdf { display: block; width: 100%; height: 60vh; border: 0; }
.viewer .hint { padding: 20px; margin: 0; }

/* A search hit's own words, with the index's [ ] markers turned into marks.
   The cell wraps — a snippet is a sentence, not a column value. */
td.snip { white-space: normal; max-width: 460px; color: var(--ink-60); }
td.snip mark { background: var(--paper-deep); color: var(--ink); padding: 0 2px; border-radius: var(--r-sm); }

/* ============================== plain ==============================
   The testing area (#/plain, reached from System only). The one screen
   allowed to look like a tool: the prompt and the answer are monospace and
   nothing about the answer is dressed, because the dressing is what the
   screen exists to remove. Everything else reuses the band and setting-row
   vocabulary above. */
.plain-in textarea {
  display: block; width: 100%; box-sizing: border-box;
  border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise); padding: 14px 16px;
  font-family: var(--mono); font-size: var(--t-small); line-height: 1.55;
  resize: vertical; min-height: 120px;
}
.plain-in textarea:focus { outline: none; border-color: var(--ink); }
.plain-acts { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.plain-acts .hint { margin-top: 0; }
/* ⛔ `hidden` alone does not hide a .btn — its display:inline-flex outranks
   the attribute's UA display:none. Rendered 2026-09-20: Stop showed at rest.
   Stop is only offered while something is running. */
.plain-acts .btn[hidden] { display: none; }
.plain-who {
  margin-top: 28px; font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-40);
}
.plain-who b { font-weight: var(--w-medium); color: var(--amber); }
.plain-out {
  margin: 10px 0 0; padding: 16px 18px; min-height: 64px;
  border: 1px solid var(--alu); border-radius: var(--r-md); background: var(--paper-deep);
  font-family: var(--mono); font-size: var(--t-small); line-height: 1.55; color: var(--ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* A waiting mark while nothing has arrived yet, so "waiting" reads as
   waiting rather than as an empty box. */
.plain-out.plain-wait:empty::after { content: "▍"; color: var(--ink-40); }
.band.two { grid-template-columns: repeat(2, 1fr); }
.plain-band { margin-top: 14px; }

/* ── Help: the illustrated guide (2026-09-29) ────────────────────────────────
   Founder: "I want a help screen which has an illustrated guide to using the
   Ox1." Two constraints shaped everything below, and neither is decorative.

   1. IT IS READ ON THE BOX'S OWN SCREEN, WHICH MAY BE A TELEVISION ACROSS A
      ROOM, and also on a phone held by somebody standing beside it. So the
      measure is capped in ch rather than px, and the two-column figure layout
      collapses rather than shrinking the drawing.
   2. IT MUST PRINT. The founder asked for an "operating/help guide" as well as
      a screen, and the cheapest honest way to have both is one document that
      prints properly — see the @media print block at the end, which is the
      whole of the printed guide's design. */
.help .hhead { margin-bottom: 30px; }
.help .hhead h1 { font-size: var(--t-title-1); font-weight: var(--w-regular); letter-spacing: -.015em; margin-bottom: 8px; }
.help .hhead p { color: var(--ink-60); font-size: var(--t-small); max-width: 62ch; }
.help .hsec { margin-bottom: 42px; }
.help .hlead { color: var(--ink-60); font-size: var(--t-small); max-width: 62ch; margin-bottom: 16px; }
.help .hnote { color: var(--ink-60); font-size: var(--t-small); max-width: 62ch; margin-top: 14px; }
.help .hsub { font-size: var(--t-body); font-weight: var(--w-medium); margin: 26px 0 12px; }

/* Prose beside a drawing. The drawing is second in the source order so that a
   screen reader and a narrow screen both meet the words first — the pictures
   carry nothing the prose does not say. */
.help .hgrid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px; align-items: start; }
.help .hprose p { font-size: var(--t-body); line-height: 1.6; margin-bottom: 12px; max-width: 54ch; }
.help .hprose p:last-child { margin-bottom: 0; }
.help .hart { padding-top: 4px; }
.help .hx { width: 100%; height: auto; }
.help .hx-lbl { font-size: var(--t-caption); fill: var(--ink-60); font-family: var(--font); }
.help .hx-cap { font-size: var(--t-caption); fill: var(--ink-40); font-family: var(--font); letter-spacing: normal; }
.help .hx-num { font-size: var(--t-caption); fill: var(--paper); font-family: var(--font); font-weight: var(--w-medium); }
.help .hx-code { font-size: 15px; fill: var(--ink); font-family: var(--mono); letter-spacing: .12em; }

.help .hsteps { margin: 6px 0 0 18px; }
.help .hsteps li { font-size: var(--t-body); line-height: 1.6; margin-bottom: 10px; max-width: 54ch; }

/* The addresses. ⛔ THE FIRST ONE IS MARKED AND THE REST ARE NOT — the entire
   complaint being answered here is that five equal-looking addresses is the
   same as none. Ranking them is the feature. */
.help .hways { list-style: none; margin: 0; padding: 0; }
.help .hway {
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  padding: 12px 14px; margin-bottom: 10px; background: var(--paper-raise);
}
.help .hway--best { border-color: var(--ink); border-left-width: 3px; }
.help .hway-top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.help .hway-url {
  font-family: var(--mono); font-size: var(--t-body); color: var(--ink);
  word-break: break-all;
}
.help .hway-tag {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  color: var(--paper); background: var(--ink); border-radius: var(--r-sm); padding: 2px 6px;
}
.help .hway-lbl { font-size: var(--t-caption); color: var(--ink-60); margin-top: 4px; }
.help .hway-note { font-size: var(--t-caption); color: var(--ink-60); margin-top: 6px; max-width: 64ch; }

.help .hqr { margin-top: 18px; display: flex; gap: 16px; align-items: flex-start; }
.help .hqr-img { width: 132px; flex: 0 0 auto; }
.help .hqr-img svg { width: 100%; height: auto; display: block; }
.help .hqr-cap { font-size: var(--t-caption); color: var(--ink-60); max-width: 46ch; }

/* The rail tour and the troubleshooting list share a shape: a short label and
   a sentence. One rule, so they cannot drift apart visually. */
.help .hrail { margin: 0; }
.help .hrail dt { font-weight: var(--w-medium); font-size: var(--t-small); margin-top: 14px; }
.help .hrail dt:first-child { margin-top: 0; }
.help .hrail dd { margin: 4px 0 0; font-size: var(--t-small); color: var(--ink-60); line-height: 1.6; max-width: 66ch; }

.help .happs { list-style: none; margin: 0; padding: 0; }
.help .happs li {
  padding: 12px 0; border-bottom: 1px solid var(--alu-soft);
  display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 16px; align-items: baseline;
}
.help .happs li:last-child { border-bottom: 0; }
.help .happs b { font-weight: var(--w-medium); font-size: var(--t-small); }
.help .happs span { font-size: var(--t-small); color: var(--ink-60); line-height: 1.6; }

.help .hpromise p {
  font-size: var(--t-body); line-height: 1.6; margin-bottom: 14px; max-width: 62ch;
  padding-left: 14px; border-left: 2px solid var(--local);
}
.help .hpromise p:last-child { margin-bottom: 0; }

/* Narrow: a phone held beside the box, or the box's own screen at 375. The
   figures go under the prose rather than shrinking — a diagram at half width
   on a phone is a diagram nobody can read. */
@media (max-width: 860px) {
  .help .hgrid { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  .help .happs li { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .help .hqr { flex-direction: column; }
}

/* ⭐ THE PRINTED GUIDE. This is not a nicety bolted on: the founder asked for a
   help SCREEN and an operating GUIDE, and one document that prints correctly is
   an honest way to be both. What goes is everything that is a control rather
   than information — the rail, the topbar, the retry buttons — because on paper
   a control is a lie. Colours flatten to ink so a mono printer keeps the
   ranking that the colour was carrying. */
@media print {
  .rail, .topbar, .crumb, .screensaver, button, .btn { display: none !important; }
  .page, .help .page { max-width: none; padding: 0; }
  .help .hsec { break-inside: avoid; margin-bottom: 26px; }
  .help .hgrid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .help .hway { background: none; border-color: #999; break-inside: avoid; }
  .help .hway--best { border-left-width: 4px; border-left-color: #000; }
  .help .hway-tag { background: none; color: #000; border: 1px solid #000; }
  .help .hpromise p { border-left-color: #000; }
  a[href^="#"]::after { content: ""; }
}

/* ── OxOnTV: the channel lineup (2026-09-29) ─────────────────────────────────
   Replaces the "not built yet" block for channels. The routes have existed
   since 2026-09-21; this is the glass catching up. Read on a wall-mounted
   television as often as on a laptop, so every control is a real target and
   nothing depends on hover to be discoverable. */
.tvch-list { list-style: none; margin: 0; padding: 0; }
.tvch {
  display: grid; grid-template-columns: 52px minmax(0, 1fr) auto;
  gap: 14px; align-items: center;
  padding: 12px 0; border-bottom: 1px solid var(--alu-soft);
}
.tvch:last-child { border-bottom: 0; }
/* ⭐ THE NUMBER IS THE LOUDEST THING IN THE ROW, because a lineup is a thing
   people navigate by number. It is the field om-oxnotv lost eight days to. */
.tvch-pos {
  font-family: var(--mono); font-size: var(--t-title-3); color: var(--ink);
  text-align: right; font-variant-numeric: tabular-nums;
}
.tvch-name { font-size: var(--t-body); font-weight: var(--w-medium); }
.tvch-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 4px; }
.tvch-meta code { font-family: var(--mono); font-size: var(--t-caption); color: var(--ink-40); }
.tvch-aud {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  border: 1px solid var(--alu-soft); border-radius: var(--r-sm); padding: 1px 6px; color: var(--ink-60);
}
/* A children's channel is marked, and an adult one is not. Marking both would
   make the distinction disappear into decoration on a long list. */
.tvch-aud--child { border-color: var(--local); color: var(--local); }
.tvch-def { font-size: var(--t-caption); color: var(--ink-60); }
/* ⛔ "Nothing is programming this yet" is AMBER, not grey. It is a real and
   actionable state of a real channel — the row exists and nothing will play on
   it — and greying it out would file it under decoration. */
.tvch-def--none { color: var(--amber); }
.tvch-acts { display: flex; gap: 8px; }
.tvch-rowerr {
  grid-column: 1 / -1; font-size: var(--t-caption); color: var(--red); margin-top: 6px;
}
/* The armed state of Remove. The word changes, so the colour is reinforcement
   and never the only signal. */
.is-armed { color: var(--red); border-color: var(--red); }

.tvch-form {
  border: 1px solid var(--ink); border-radius: var(--r-md);
  padding: 16px; margin-bottom: 16px; background: var(--paper-raise);
}
.tvch-form-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px;
}
.tvch-form label { display: flex; flex-direction: column; gap: 5px; font-size: var(--t-small); color: var(--ink-60); }
.tvch-form input, .tvch-form select {
  font-family: var(--font); font-size: var(--t-body); color: var(--ink);
  background: var(--paper); border: 1px solid var(--alu-soft);
  border-radius: var(--r-md); padding: 8px 10px;
}
.tvch-form input:focus-visible, .tvch-form select:focus-visible {
  outline: 2px solid var(--ink); outline-offset: 1px;
}
/* Readonly is the short name of an existing channel: it can never change,
   because televisions follow it. Shown rather than hidden, so the owner can
   read the value they are being told they cannot edit. */
.tvch-form input[readonly] { background: var(--paper-deep); color: var(--ink-60); }
.tvch-hint { font-size: var(--t-caption); color: var(--ink-40); line-height: 1.45; }
.tvch-form-acts { display: flex; gap: 10px; align-items: center; margin-top: 14px; flex-wrap: wrap; }
.tvch-err { font-size: var(--t-caption); color: var(--red); }
.tv-empty { font-size: var(--t-small); color: var(--ink-60); max-width: 60ch; }
/* The add-a-subscription form (2026-09-29) reuses the channel form's shell.
   ⛔ `hidden` on a label needs saying here: the label rule above sets
   display:flex, which beats the browser's own [hidden] rule, so without this
   line every field for every kind would show at once — the exact clutter the
   form exists to avoid. */
.tvch-form label[hidden] { display: none; }
.tv-sub-form { margin-top: 16px; }
/* The label rule above stacks a label's children in a column; a checkbox and
   its words belong on one line, box first. */
.tv-sub-form .tv-check {
  flex-direction: row; align-items: center; margin-top: 14px;
  font-size: var(--t-small); color: var(--ink);
}
.tv-sub-saved {
  margin-top: 16px; padding: 12px 16px; border-radius: var(--r-md);
  border: 1px solid var(--alu-soft); background: var(--paper-raise);
  font-size: var(--t-small); line-height: 1.5; color: var(--ink);
}

@media (max-width: 720px) {
  .tvch { grid-template-columns: 40px minmax(0, 1fr); }
  .tvch-acts { grid-column: 2; justify-content: flex-start; }
  .tvch-form-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ── Ask: attaching a document, and taking the answer away (2026-09-29) ───────
   Roadmap items 2 and 4 of docs/oxongpt/01-THE-ROADMAP.md, from the founder's
   2026-09-19 brief ("document uploads, images, downloads") and his 2026-09-29
   ask for Ask to be as rich as the web version.

   ⛔ THE VISUAL WEIGHT HERE IS DELIBERATE AND IT IS LOW. An attachment row is
   NOT a celebration — it is a receipt. The one row that must catch the eye is
   the one saying the AI CANNOT read a file, because that is the state where a
   confident-looking screen would let somebody ask a question the box cannot
   answer from a document they believe it has. */
.attach-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex: 0 0 auto;
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  color: var(--ink-60); cursor: pointer; background: var(--paper);
}
.attach-btn:hover { color: var(--ink); border-color: var(--alu); }
.attach-btn:focus-within { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The wait while a file crosses the network. Not a spinner: the row below
   already says what is happening in words. */
.attach-btn.is-busy { opacity: .5; pointer-events: none; }

/* Visually hidden, still read aloud. The paperclip needs a name for anyone who
   cannot see it, and a `title` alone is not reliably announced. */
.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.attached { margin-bottom: 10px; }
.attached-list { list-style: none; margin: 0; padding: 0; }
.attached-row {
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  background: var(--paper-raise); padding: 9px 12px; margin-bottom: 8px;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
}
/* ⛔ AMBER AND A LEFT EDGE, because "saved but unreadable" is the state this
   whole feature can most easily lie about. Not red: nothing failed, and the
   owner's file is safe on the computer. */
.attached-row--no { border-left: 3px solid var(--amber); }
.attached-name { font-size: var(--t-small); color: var(--ink); word-break: break-all; }
.attached-tag {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  border-radius: var(--r-sm); padding: 2px 6px; white-space: nowrap;
}
.attached-tag--ok { color: var(--local); border: 1px solid var(--local); }
.attached-tag--no { color: var(--amber); border: 1px solid var(--amber); }
.attached-why {
  flex-basis: 100%; font-size: var(--t-caption); color: var(--ink-60); line-height: 1.5;
  max-width: 62ch;
}
/* A quiet button, and its words say what it does — it does not delete. */
.attached-x {
  margin-left: auto; background: none; border: 0; padding: 2px 4px;
  font: inherit; font-size: var(--t-caption); color: var(--ink-40); cursor: pointer;
}
.attached-x:hover { color: var(--ink); text-decoration: underline; }
.attached-foot { font-size: var(--t-caption); color: var(--ink-60); max-width: 66ch; }
.attached-foot--err { color: var(--red); }

/* The drop target is the whole composer area, and it says so only while a file
   is over it. A permanent dashed rectangle would put a piece of chrome on the
   screen the owner uses most, to advertise a gesture they either know or do
   not. */
.ask-wrap.is-dropping .composer {
  outline: 2px dashed var(--local); outline-offset: 6px;
}

/* What you can do with an answer. Appears only once an answer has finished —
   there is nothing to save while it is still arriving. */
.answer-acts {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px;
}
.answer-acts__said { font-size: var(--t-caption); color: var(--ink-60); }

@media (max-width: 720px) {
  .attached-x { margin-left: 0; }
}

/* ⭐ OxOnOS's own app marks (2026-09-29). Inline SVG, so they share the size and
   radius of a packaged app's <img> icon exactly — the whole point of the
   founder's ask was that our own screens looked less finished than software we
   did not write, sitting next to it on the same grid. Same box, same rhythm. */
.app-card__icon--own { display: block; }

/* ── The hero: the box, and the state of it, at a glance (2026-09-29) ────────
   Founder on the first attempt: "i wanted it more control panel style, like a
   dashboard with a bit more flare rather than small scratchy font … a cross
   between the tiles we originally had and what you have done now … something i
   can look at and instantly know the state of the box."

   ⭐ SO THIS IS A DASHBOARD, NOT A SPEC SHEET. macOS's About This Mac is an
   IDENTITY panel — read once, closed. This is opened every day and has to be
   legible from across a room, which is why the readings are tiles with large
   numerals and the portrait shares the panel rather than sitting above a
   paragraph of 13px type. */
.hero {
  display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  gap: 30px; align-items: center;
  /* 24px above, the same air the check grid gets in this slot: without it the
     sub line's last word sat 8px off the panel's top rule (rendered 1920/4K,
     2026-09-30). */
  margin: 24px 0 26px; padding: 26px 28px;
  background: var(--paper-raise);
  border: 1px solid var(--alu); border-radius: var(--r-lg);
}
.hero-art { display: flex; justify-content: center; }
.hero-img {
  width: 100%; max-width: 236px; height: auto; display: block;
  /* The render is a dark object on transparency. A soft shadow seats it on the
     paper rather than leaving it floating, and survives a theme change because
     it is a shadow rather than a painted backdrop. */
  filter: drop-shadow(0 16px 26px rgba(0, 0, 0, .26));
}
.hero-panel { min-width: 0; }
.hero-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.hero-name {
  font-size: var(--t-title-1); font-weight: var(--w-regular); letter-spacing: -.02em; line-height: 1.05;
}
.hero-sub { font-size: var(--t-caption); color: var(--ink-60); margin-top: 4px; letter-spacing: normal; }
/* The verdict, as a pill, so the eye lands on it before the numbers. Its state
   comes from the health probes and never from a second opinion formed here. */
.hero-state {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font-size: var(--t-caption); font-weight: var(--w-medium); padding: 6px 12px; border-radius: var(--r-pill);
  border: 1px solid var(--alu-soft); background: var(--paper);
}
.hero-state--ok   { color: var(--local); border-color: var(--local); }
.hero-state--warn { color: var(--amber); border-color: var(--amber); }
.hero-state--bad  { color: var(--red);   border-color: var(--red); }
.hero-state--idle { color: var(--ink-60); }

/* ⭐ THE READINGS. Four tiles, big numerals, in the same grammar as the check
   cells below them — label in small caps, value large — so the screen reads as
   one instrument rather than two designs meeting in the middle. */
.hstats {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  overflow: hidden; background: var(--paper);
}
.hstat {
  padding: 14px 16px 15px; border-right: 1px solid var(--alu-soft);
  display: flex; flex-direction: column; gap: 5px; min-width: 0;
}
.hstat:last-child { border-right: none; }
.hstat-label {
  font-size: var(--t-caption); font-weight: var(--w-medium); letter-spacing: normal;
  text-transform: none; color: var(--ink-60);
  display: flex; align-items: center; gap: 6px;
}
/* The flare the founder asked for lives here: a light, tabular numeral at a
   size you can read standing up. */
.hstat-value {
  font-size: var(--t-title-1); font-weight: var(--w-display); letter-spacing: -.02em; line-height: 1.05;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.hstat-unit { font-size: var(--t-body); margin-left: 2px; color: var(--ink-60); }
/* ⛔ A READING THE BOX COULD NOT TAKE IS SHOWN, NOT DROPPED, and at a size that
   does not pretend to be a measurement. A missing tile reads as "this computer
   has no such thing"; these words read as what they are. */
.hstat-value--none { font-size: var(--t-body); color: var(--ink-40); font-style: italic; }
.hstat-note { font-size: var(--t-caption); color: var(--ink-40); line-height: 1.35; }

.hero-spec {
  margin-top: 16px; font-size: var(--t-caption); color: var(--ink-60); line-height: 1.5;
}
.hero-foot {
  margin-top: 6px; font-size: var(--t-caption); color: var(--ink-40);
  font-variant-numeric: tabular-nums;
}

/* ⚠️ FOUR TILES NEED ~90px OF INNER WIDTH EACH, AND BELOW ~1180px THE PANEL
   DOES NOT HAVE IT. Measured 2026-09-30 at 1024: the panel beside a 300px
   portrait is 326px, so each tile had 49px inside its padding and "2.0 TB"
   (83px at 30px/300) ran 34px past its own rule into the next tile, with the
   last one clipped by the band's overflow. Two-by-two from 1180 down, with the
   portrait still beside the panel; the portrait only stacks at 900 below. */
@media (max-width: 1180px) {
  .hstats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .hstat:nth-child(2) { border-right: none; }
  .hstat:nth-child(-n+2) { border-bottom: 1px solid var(--alu-soft); }
}
/* Narrow: the portrait goes above the panel rather than shrinking beside it. A
   half-width box render on a phone is a picture of nothing. */
@media (max-width: 900px) {
  .hero { grid-template-columns: minmax(0, 1fr); gap: 20px; padding: 22px 18px; }
  .hero-img { max-width: 168px; }
  .hero-name { font-size: var(--t-title-2); }
  .hstat-value { font-size: var(--t-title-2); }
}
/* On paper the render is a large block of ink carrying no information. */
@media print { .hero { grid-template-columns: minmax(0, 1fr); } .hero-art { display: none; } }

/* ── Naming the computer (2026-09-29) ────────────────────────────────────────
   Founder: "it makes the box belong to the user or group which is important."
   The field sits in a settings row rather than in its own screen, because it is
   a property of the machine and not a journey. */
.setrow--stack { align-items: flex-start; flex-wrap: wrap; }
.idform { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.idform input {
  font: inherit; font-size: var(--t-small); color: var(--ink); background: var(--paper);
  border: 1px solid var(--alu-soft); border-radius: var(--r-md);
  padding: 7px 10px; min-width: 190px;
}
.idform input:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.idsaid { font-size: var(--t-caption); color: var(--ink-60); }
.idsaid--ok { color: var(--local); }
/* ⚠️ Amber, not red: a refused name is not a fault in the machine, and red here
   would read as something being broken rather than something being declined. */
.idsaid--err { color: var(--amber); }

/* ── OxOnGPT: a rendered answer (2026-09-29) ─────────────────────────────────
   Founder: "correctly formatted and stylised — no **Test** rubbish but
   correctly formatted, with tables etc". The markup comes from gpt/render.js
   (escape-first, no library); these rules give it the product's own paper and
   ink rather than the browser's defaults. Scoped to `.md` so nothing here
   leaks into the owner's own bubble, which is plain text on purpose.
   ⚠️ NO ACCENT COLOURS. Calm is the wedge: a table is rules and zebra, a
   heading is weight and space, code is the mono face on raised paper. */
.md > :first-child { margin-top: 0; }
.md > :last-child { margin-bottom: 0; }
.md p { margin: 0 0 12px; }
.md h2, .md h3, .md h4, .md h5, .md h6 {
  font-weight: var(--w-medium); letter-spacing: -.01em; line-height: 1.3; margin: 22px 0 8px;
}
.md h2 { font-size: var(--t-title-3); }
.md h3 { font-size: var(--t-lead); }
.md h4, .md h5, .md h6 { font-size: var(--t-body); }
.md ul, .md ol { margin: 0 0 12px; padding-left: 24px; }
.md li { margin: 3px 0; }
.md li > ul, .md li > ol { margin: 4px 0 0; }
.md strong { font-weight: var(--w-bold); }
.md em { font-style: italic; }
.md del { color: var(--ink-40); }
.md hr { border: 0; border-top: 1px solid var(--alu-soft); margin: 20px 0; }
.md blockquote {
  margin: 0 0 12px; padding: 2px 0 2px 14px; border-left: 2px solid var(--alu);
  color: var(--ink-60);
}
.md a { color: var(--oxford); text-decoration: underline; text-underline-offset: 2px; }
.md a:hover { color: var(--ink); }
.md code {
  font-family: var(--mono); font-size: var(--t-small);
  background: var(--paper-deep); border-radius: var(--r-sm); padding: 1px 5px;
}
.md pre {
  margin: 0 0 14px; padding: 12px 14px; overflow-x: auto;
  background: var(--paper-raise); border: 1px solid var(--alu-soft); border-radius: var(--r-lg);
  white-space: pre;
}
.md pre code { background: none; padding: 0; font-size: var(--t-small); line-height: 1.55; }
/* Tables: a scroller on narrow screens, rules and zebra on every screen.
   The scroller matters more than it looks — a table wider than a phone that
   cannot scroll is a table the owner cannot read. */
.md .tablewrap {
  overflow-x: auto; margin: 4px 0 16px;
  border: 1px solid var(--alu-soft); border-radius: var(--r-lg); background: var(--paper-raise);
}
.md table { border-collapse: collapse; width: 100%; font-size: var(--t-small); line-height: 1.45; }
.md th, .md td { text-align: left; padding: 9px 13px; border-bottom: 1px solid var(--alu-soft); vertical-align: top; }
.md th {
  font-size: var(--t-small); font-weight: var(--w-medium); letter-spacing: .02em; color: var(--ink-60);
  background: var(--paper-deep); white-space: nowrap;
}
.md tbody tr:nth-child(even) td { background: rgba(0,0,0,.025); }
.md tbody tr:last-child td { border-bottom: 0; }
.md td code { white-space: nowrap; }

/* ---------- Computers: THE PICTURE (founder, 2026-09-29) ----------
   Design intent is the block at the top of ui/js/screens/computers.js; this
   is only its dress. ⛔ NO NEW HUE, NO NEW TOKEN. The four link looks are the
   four status colours that already exist (--local, --amber, --red, --ink-40),
   told apart by DASH as well as colour so colour is never the only carrier.
   Cards sit on raised paper inside the brand's own hairlines. The one glow
   (a card that just answered) is --local at low alpha, not a new colour. */
.netmap { margin-top: 30px; }
.netmap-kicker {
  font-size: var(--t-caption); font-weight: var(--w-medium);
  letter-spacing: normal; text-transform: none;
  color: var(--ink-40);
}
.netmap-title {
  font-weight: var(--w-display); font-size: var(--t-title-1); letter-spacing: -.02em;
  line-height: 1.05; margin-top: 6px; overflow-wrap: anywhere;
}
.netmap-count { margin-top: 10px; font-size: var(--t-lead); color: var(--ink-60); }
.node-hint { display: block; margin-top: 6px; font-size: var(--t-small); color: var(--ink-40); }
.netmap-net {
  display: flex; align-items: center; gap: 9px; margin-top: 14px;
  font-size: var(--t-small); color: var(--ink-60);
}
/* Two owner words that disagree: said in amber, because it is worth knowing
   and it is not a fault. */
.netmap-differ { margin-top: 10px; font-size: var(--t-small); color: var(--amber); max-width: 70ch; }

.netmap-grid {
  position: relative; margin-top: 26px;
  display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr) clamp(150px, 15vw, 320px) minmax(0, 1fr);
}
.netmap.alone .netmap-grid { grid-template-columns: minmax(0, 1fr); }
.netmap-self { grid-column: 1; align-self: center; }
.netmap-peers { grid-column: 3; display: flex; flex-direction: column; gap: 22px; }
/* The overlay is drawn from the cards' measured positions (drawLines). It is
   decoration for sighted readers; every fact on it is also in a card. */
.netmap-lines {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; overflow: visible;
}

.node {
  position: relative; background: var(--paper-raise);
  border: 1px solid var(--alu); border-radius: var(--r-lg);
  padding: 18px 20px 16px; max-width: 560px;
}
.node.self { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.node.peer.ok { border-color: var(--local); }
.node.peer.failed, .node.peer.unknown { border-style: dashed; }
.node.peer.unknown { border-color: var(--alu); }
.node.peer.failed { border-color: color-mix(in srgb, var(--warn) 35%, transparent); }
.node.peer.refused { border-color: var(--red); }
.node.peer.asking { border-style: dotted; }
.node-head { display: flex; align-items: flex-start; gap: 12px; }
.node-mark { width: 34px; height: 34px; flex: 0 0 auto; display: block; margin-top: 2px; }
.node-title { flex: 1; min-width: 0; }
.node-title b { display: block; font-weight: var(--w-medium); font-size: var(--t-lead); line-height: 1.3; }
.node-title b .tag { margin-left: 10px; vertical-align: middle; }
.node-id { display: block; margin-top: 3px; font-size: var(--t-small); color: var(--ink-40); }
.node-head > .tag { flex: 0 0 auto; margin-top: 6px; }
.node-line { margin-top: 12px; font-size: var(--t-small); color: var(--ink-60); line-height: 1.45; }
/* What it SAID, with the time — shown after its link has gone grey, never as
   what it holds now. */
.node-asof {
  margin-top: 12px; font-size: var(--t-caption); letter-spacing: normal;
  text-transform: none; color: var(--amber);
}
.node-caps {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--alu-soft);
  display: grid; grid-template-columns: max-content 1fr; column-gap: 16px; row-gap: 6px;
}
.node-caps span.k {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  color: var(--ink-40); margin-top: 3px; white-space: nowrap;
}
.node-caps span.v { font-size: var(--t-small); color: var(--ink); overflow-wrap: anywhere; }
/* ⛔ THE THIRD STATE IS A WORD, IN AMBER — never a dash, never a blank. */
.node-caps span.v.unlooked { color: var(--amber); }
.node-acts { margin-top: 14px; display: flex; justify-content: flex-end; }
.netmap-key {
  margin-top: 14px; display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center;
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none; color: var(--ink-40);
}
.netmap-key .key {
  display: inline-block; width: 28px; height: 0; margin-right: 8px; vertical-align: middle;
  border-top: 3px solid var(--local);
}
.netmap-key .key.failed { border-top: 2.5px dashed var(--amber); }
.netmap-key .key.unknown { border-top: 2.5px dotted var(--ink-40); }
.netmap-key .key.refused { border-top-color: var(--red); }

/* the links: four looks for four facts, dash AND colour */
.netmap-lines .link { fill: none; stroke-linecap: round; }
.netmap-lines .link.ok { stroke: var(--local); stroke-width: 3; }
.netmap-lines .link.failed { stroke: var(--amber); stroke-width: 2.5; stroke-dasharray: 9 7; }
.netmap-lines .link.unknown { stroke: var(--ink-40); stroke-width: 2.5; stroke-dasharray: 2 7; }
.netmap-lines .link.refused { stroke: var(--red); stroke-width: 3; }
.netmap-lines .link.asking { stroke: var(--ink-40); stroke-width: 2; stroke-dasharray: 6 8; opacity: .7; }
/* The route rides the link. A paper stroke behind the letters knocks the
   line out under them so the label reads on top of its own path. */
.netmap-lines .link-label {
  font: var(--w-medium) var(--t-caption)/1 var(--font); letter-spacing: normal; text-transform: none;
  fill: var(--ink-60); paint-order: stroke fill; stroke: var(--paper); stroke-width: 6px;
  stroke-linejoin: round;
}
.netmap-lines .link-label.ok { fill: var(--local); }
.netmap-lines .link-label.failed { fill: var(--amber); }
.netmap-lines .link-label.unknown { fill: var(--ink-40); }
.netmap-lines .link-label.refused { fill: var(--red); }
.netmap-lines .link-label.asking { fill: var(--ink-40); }
/* Motion only where a real event happened, and only for people who have not
   asked for none: the in-flight dash marches, and a link that just earned
   its look lights once. */
@media (prefers-reduced-motion: no-preference) {
  .netmap-lines .link.asking { animation: netmap-march 1.2s linear infinite; }
  .netmap-lines .link.lit { animation: netmap-lit 1.5s ease-out 1; }
  .node[data-lit="yes"] { animation: netmap-glow 1.5s ease-out 1; }
}
@keyframes netmap-march { to { stroke-dashoffset: -28; } }
@keyframes netmap-lit {
  0% { stroke-width: 3; opacity: 1; } 30% { stroke-width: 9; opacity: .55; } 100% { stroke-width: 3; opacity: 1; }
}
@keyframes netmap-glow {
  0% { box-shadow: 0 0 0 0 rgba(28, 103, 66, 0); }
  30% { box-shadow: 0 0 0 10px rgba(28, 103, 66, .18); }
  100% { box-shadow: 0 0 0 0 rgba(28, 103, 66, 0); }
}

/* the typed address, folded under a row's technical details */
.peer-address { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.peer-address label { flex-basis: 100%; font-size: var(--t-small); color: var(--ink-60); }
.peer-address input {
  flex: 1 1 22ch; min-width: 0; font-family: var(--mono); font-size: var(--t-small);
  padding: 8px 10px; border: 1px solid var(--alu); border-radius: var(--r-md);
  background: var(--paper-raise);
}

@media (max-width: 480px) {
  /* ⚠️ THE NAME AND THE STATUS CANNOT SHARE 227px. Beside the mark and a
     "NOT CHECKED" tag the title column was 69px on a 375 phone, and a
     one-word name ("Workshop") ran 18px past it. The status drops under the
     name, aligned to its left edge; the words stay whole. Measured 2026-09-30. */
  .node-head { flex-wrap: wrap; }
  .node-title { flex-basis: calc(100% - 46px); }
  .node-head > .tag { margin-left: 46px; margin-top: 0; }
}
@media (max-width: 760px) {
  /* One column: cards stack, the overlay goes, and every card already
     carries its route as a sentence — the same facts, no lines to mis-draw. */
  .netmap-grid { grid-template-columns: minmax(0, 1fr); row-gap: 18px; }
  .netmap-self, .netmap-peers { grid-column: 1; }
  .netmap-lines { display: none; }
  .node { max-width: none; }
  .node-caps { grid-template-columns: 1fr; row-gap: 2px; }
  .node-caps span.k { margin-top: 10px; }
  .node-caps span.k:first-child { margin-top: 0; }
}
@media (min-width: 1700px) {
  /* Across a room: the cards, the mark and the lines grow with the type. */
  .node { max-width: 780px; padding: 26px 30px 24px; }
  .node-mark { width: 44px; height: 44px; }
  .netmap-peers { gap: 30px; }
  .netmap-lines .link.ok, .netmap-lines .link.refused { stroke-width: 4; }
  .netmap-lines .link.failed, .netmap-lines .link.unknown { stroke-width: 3.5; }
  .netmap-lines .link-label { stroke-width: 8px; }
}


/* ============================ folding section =========================
   ⛔⭐ Founder, 2026-09-29: the Health block was "front and central" on the
   System screen and he hated it there. It moved down beside the hardware and
   folds — open by default, because it is a fold for the second visit, not a
   thing to go hunting for on the first.

   ⭐ THE SUMMARY SITS IN THE HEADER so a collapsed section still states its
   verdict. A fold that hides its own answer only costs a click. */
.sect-fold { border: 0; margin: 34px 0 0; }
.sect-fold > summary { list-style: none; cursor: pointer; }
.sect-fold > summary::-webkit-details-marker { display: none; }
.sect-fold__head {
  display: flex; align-items: baseline; gap: 14px;
  padding-bottom: 8px; border-bottom: 1px solid var(--alu-soft);
}
/* ⚠️ SAME GRAMMAR AS .sect h2, NOT A BOLD CAPTION. Rendered at 1920 on
   2026-09-30: a 15px/600 "Health" sat directly under the hardware band's
   "Technical details" link and read as a sub-item of it, while every other
   section on the page — Privacy, Network, Storage and hardware — is a 19px
   light heading on a rule. It is a section; it folds. Only the size and weight
   move, so the fold, the caret and the summary-in-the-header stay as ruled. */
.sect-fold__title { font-size: var(--t-title-3); font-weight: var(--w-regular); letter-spacing: -.01em; color: var(--ink); }
.sect-fold__sum { font-size: var(--t-small); color: var(--ink-40); margin-left: auto; white-space: nowrap; }
@media (max-width: 480px) { .sect-fold__sum { white-space: normal; flex-basis: 100%; margin-left: 0; } .sect-fold__head { flex-wrap: wrap; } }
/* The caret is drawn rather than relying on the platform marker, which differs
   between WebKitGTK on the appliance and every desktop browser. */
.sect-fold__head::before {
  content: ''; width: 7px; height: 7px; flex: 0 0 auto;
  border-right: 1.6px solid var(--ink-40); border-bottom: 1.6px solid var(--ink-40);
  transform: rotate(-45deg); transition: transform .15s ease; margin-right: 2px;
}
.sect-fold[open] > summary .sect-fold__head::before { transform: rotate(45deg); }
.sect-fold > summary:hover .sect-fold__title { color: var(--accent, var(--ink)); }
.sect-fold__body { padding-top: 10px; }
@media (prefers-reduced-motion: reduce) {
  .sect-fold__head::before { transition: none; }
}

/* ==========================================================================
   WIDE GLASS, SECOND PASS — the surfaces added on 2026-09-29 (visual pass,
   2026-09-30, before the RH002 demo)
   ==========================================================================
   The hero, the Apps cards and the OxOnTV screen were all written in px at
   laptop size and never given the ≥1700 / ≥2400 tiers the rest of this file
   has. Rendered at 3840×2160 — the panel the box is actually plugged into —
   the hero's readings (30px) were SMALLER than the check-grid values under
   them (40px), its labels were 10.5px, and the Apps cards sat at laptop scale
   beside an 88px heading: the "small scratchy font" the founder named on
   2026-09-29, on the very panel built to answer it. Nothing below 1700 moves. */
@media (min-width: 1700px) {
  .hero { grid-template-columns: minmax(0, 360px) minmax(0, 1fr); gap: 40px; padding: 34px 38px; }
  .hero-img { max-width: 300px; }
  .hero-name { font-size: var(--t-display); }
  .hero-sub { font-size: var(--t-small); margin-top: 6px; }
  .hero-state { font-size: var(--t-small); padding: 8px 16px; }
  .hstat { padding: 20px 22px 21px; gap: 6px; }
  .hstat-label { font-size: var(--t-caption); }
  .hstat-value { font-size: var(--t-display); }
  .hstat-unit { font-size: var(--t-title-3); }
  .hstat-value--none { font-size: var(--t-lead); }
  .hstat-note { font-size: var(--t-small); }
  .hero-spec { font-size: var(--t-small); margin-top: 20px; }
  .hero-foot { font-size: var(--t-caption); }

  .app-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }
  .app-grid--feature { grid-template-columns: repeat(auto-fill, minmax(520px, 1fr)); }
  .app-card { padding: 28px 28px 26px; border-radius: var(--r-lg); }
  .app-card--feature { padding: 32px 32px 30px; }
  .app-card__mark { width: 80px; height: 80px; }
  .app-card--feature .app-card__mark { width: 96px; height: 96px; }
  .app-card__mono { font-size: var(--t-display); }
  .app-card__name { font-size: var(--t-lead); margin-top: 18px; }
  .app-card--feature .app-card__name { font-size: var(--t-title-3); }
  .app-card__what { font-size: var(--t-small); min-height: calc(2 * 1.5 * var(--fs-sm)); }
  .app-card__state, .app-card__where { font-size: var(--t-small); }
  .app-card__open { font-size: var(--t-small); min-height: 48px; padding: 11px 28px; }
  .app-chip { font-size: var(--t-caption); padding: 6px 13px 6px 11px; }
  .apps-sect { margin-top: 72px; padding-top: 32px; }
  .apps-sect--first { margin-top: 48px; }
  .app-frame__name { font-size: var(--t-body); }
  .app-frame__note { font-size: var(--t-small); }

  .tv-cell { padding: 26px 28px 28px; }
  .tv-cell__label { font-size: var(--t-caption); }
  .tv-cell__value { font-size: var(--t-title-1); }
  .tv-cell__foot { font-size: var(--t-small); }
  .tv-prov { padding: 26px 28px 28px; }
  .tv-prov__name { font-size: var(--t-lead); }
  .tv-prov__badge { font-size: var(--t-caption); }
  .tv-prov__note, .tv-saved, .tv-failed, .tv-empty, .tv-soon p, .tvch-hint,
  .tvch-err, .tv-sub-saved, .tvch-form label, .tvch-def, .tvch-rowerr { font-size: var(--t-small); }
  .tv-field > span { font-size: var(--t-small); }
  .tv-field input { font-size: var(--t-body); max-width: 640px; }
  .tv-check { font-size: var(--t-body); }
  .tv-soon__title { font-size: var(--t-lead); }
  .tvch { grid-template-columns: 68px minmax(0, 1fr) auto; padding: 16px 0; }
  .tvch-pos { font-size: var(--t-title-2); }
  .tvch-name { font-size: var(--t-lead); }
  .tvch-meta code { font-size: var(--t-small); }
  .tvch-aud { font-size: var(--t-caption); }
  .tvch-form input, .tvch-form select { font-size: var(--t-body); }
}
@media (min-width: 2400px) {
  .hero { grid-template-columns: minmax(0, 440px) minmax(0, 1fr); gap: 52px; padding: 44px 48px; }
  .hero-img { max-width: 380px; }
  .hero-name { font-size: var(--t-display); }
  .hstat { padding: 26px 28px 27px; }
  .hstat-value { font-size: var(--t-title-1); }
  .hstat-unit { font-size: var(--t-title-2); }
  .app-grid { grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 24px; }
  .app-grid--feature { grid-template-columns: repeat(auto-fill, minmax(680px, 1fr)); }
  .app-card { padding: 36px 36px 32px; }
  .app-card__mark, .app-card--feature .app-card__mark { width: 96px; height: 96px; }
  .app-card__mono { font-size: var(--t-display); }
  .tvch { grid-template-columns: 80px minmax(0, 1fr) auto; padding: 19px 0; }
  .tvch-pos { font-size: var(--t-title-1); }
}

/* ---------- a Kit's own page (founder, 2026-10-02 16:54) ----------
   "As if it was a 3rd party kit downloaded": a store-page header, then About,
   What it does, the flow (the infographic) and the team. Ink and paper only —
   the Kit's identity is its NAME and its people, not a colour of its own. */
.kb-hero { display: flex; gap: 18px; align-items: center; margin: 6px 0 10px; }
.kb-mark { flex: 0 0 auto; width: 64px; height: 64px; border-radius: var(--r-lg);
  background: var(--ink); color: var(--paper); display: grid; place-items: center;
  font-size: var(--t-title-2); font-weight: var(--w-bold); letter-spacing: .02em; }
.kb-name { font-size: var(--t-title-1); font-weight: var(--w-bold); letter-spacing: -.01em; line-height: 1.1; }
.kb-by { font-size: var(--t-small); color: var(--ink-60); margin-top: 3px; }
.kb-facts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.kb-facts span { font-size: var(--t-caption); font-weight: var(--w-bold); letter-spacing: normal; text-transform: none;
  padding: 3px 9px; border-radius: var(--r-pill); border: 1px solid var(--alu-soft); color: var(--ink-60); }
.kb-tag { font-size: var(--t-lead); line-height: 1.5; max-width: 62ch; margin: 4px 0 18px; }
.kb-about p { max-width: 68ch; line-height: 1.6; margin: 0 0 10px; }
.kb-does { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.kb-do { background: var(--paper-raise); border: 1px solid var(--alu-soft); border-radius: var(--r-md); padding: 14px 16px; }
.kb-do b { display: block; font-size: var(--t-body); margin-bottom: 4px; }
.kb-do p { margin: 0; font-size: var(--t-small); color: var(--ink-60); line-height: 1.5; }

/* The infographic: a numbered rail of stages, joined by a line, each carrying
   its own seats. Left-to-right on a wide screen, top-to-bottom on a narrow one. */
.kb-flow { list-style: none; padding: 0; margin: 4px 0 10px; display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; position: relative; }
.kb-stage { position: relative; background: var(--paper-raise); border: 1px solid var(--alu-soft);
  border-radius: var(--r-md); padding: 14px 12px 12px; }
.kb-stage:not(:last-child)::after { content: "→"; position: absolute; right: -10px; top: 14px;
  width: 10px; text-align: center; color: var(--ink-40); font-size: var(--t-small); }
.kb-step { width: 24px; height: 24px; border-radius: 50%; background: var(--ink); color: var(--paper);
  display: grid; place-items: center; font-size: var(--t-caption); font-weight: var(--w-bold); }
.kb-stitle { font-weight: var(--w-bold); font-size: var(--t-body); margin: 8px 0 4px; }
.kb-stext { font-size: var(--t-caption); color: var(--ink-60); line-height: 1.45; margin: 0 0 10px; }
.kb-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.kb-chip { display: inline-flex; align-items: center; gap: 3px; font-size: var(--t-caption); font-weight: var(--w-bold);
  padding: 3px 8px; border-radius: var(--r-pill); background: var(--paper); border: 1px solid var(--alu); white-space: nowrap; }
.kb-chip i { font-style: normal; font-size: var(--t-caption); }
.kb-chip.block { border-color: var(--red); color: var(--red); background: var(--red-tint); }
.kb-chip.outside { border-style: dashed; color: var(--ink-60); }
.kb-legend { display: flex; flex-wrap: wrap; gap: 8px 22px; font-size: var(--t-caption); color: var(--ink-60);
  margin: 4px 0 6px; align-items: center; }
.kb-legend > span { display: inline-flex; align-items: center; gap: 6px; }

.kb-team { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
.kb-agent { background: var(--paper-raise); border: 1px solid var(--alu-soft); border-radius: var(--r-md); padding: 12px 14px; }
.kb-agent.block { border-color: color-mix(in srgb, var(--bad) 30%, transparent); }
.kb-agent.outside { border-style: dashed; }
.kb-aname { font-weight: var(--w-bold); font-size: var(--t-body); display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.kb-flag { font-size: var(--t-caption); font-weight: var(--w-bold); letter-spacing: normal; text-transform: none; color: var(--red); }
.kb-agent.outside .kb-flag { color: var(--ink-60); }
.kb-arole { font-size: var(--t-caption); color: var(--ink-60); margin-top: 2px; }
.kb-agent p { font-size: var(--t-small); line-height: 1.45; margin: 6px 0 0; }
.kb-note { font-size: var(--t-small); color: var(--ink-60); max-width: 72ch; line-height: 1.5;
  border-left: 3px solid var(--alu); padding-left: 12px; margin: 18px 0 6px; }

/* Wide enough for every stage on one line: the flow reads as ONE journey. */
@media (min-width: 1180px) {
  .kb-flow { grid-template-columns: repeat(var(--n, 6), minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .kb-flow { grid-template-columns: 1fr; }
  .kb-stage:not(:last-child)::after { content: "↓"; right: auto; left: 50%; top: auto; bottom: -13px; }
  .kb-name { font-size: var(--t-title-2); }
}
/* re-audit N-10: the Kit page's actions are ONE width — equal grid columns
   sized by the widest label; stacked full width on a phone. */
.kb-acts { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--s-3);
  width: max-content; max-width: 100%; margin: 0 0 var(--s-5); }
@media (max-width: 760px) { .kb-acts { grid-auto-flow: row; width: 100%; } }

/* ---------- clyde (js/screens/clyde.js) ----------
   Screen v2 (2026-10-04): the stage card, the four chip rows, the always-on
   notes and the micro-label status overlay are retired. The cv-* component
   set lives in css/v2-clyde.css (spec pod/dev/specs/clyde-screen-v2.md §4). */
.srcs__label {
  align-self: center; font-size: var(--t-caption); letter-spacing: normal;
  text-transform: none; color: var(--ink-40); margin-right: 4px;
}

/* ---------- Clyde remembers (2026-10-03, migration 14) ----------
   The Remember row under a question sits under ITS bubble, on the right, and
   is pulled up into the thread gap so it reads as belonging to the message. */
.msg-q-acts { align-self: flex-end; margin-top: -18px; }
.msg-q-acts .answer-acts__said { text-align: right; max-width: 60ch; }
.answer-acts__offer { font-size: var(--t-small); color: var(--ink-60); font-style: italic; max-width: 52ch; }
.msg-a .body blockquote {
  margin: 0 0 8px; padding: 6px 14px; border-left: 3px solid var(--alu);
  color: var(--ink);
}
/* A note chip is a different kind of thing from a file chip: say so quietly. */
.src.src--note { border-style: dashed; }
.clyde-memory { max-width: 760px; margin-top: 44px; }
.clyde-memory h2 { font-size: var(--t-lead); font-weight: var(--w-medium); margin: 0; }
.clyde-memory .setrow .acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.clyde-note-row__text { white-space: pre-wrap; overflow-wrap: anywhere; }
.clyde-note-row.is-target { background: var(--paper-deep); }
.clyde-note-edit {
  width: 100%; font: inherit; font-size: var(--t-body); padding: 8px 10px;
  border: 1px solid var(--alu); border-radius: var(--r-sm); background: var(--paper-raise); resize: vertical;
}
@media (min-width: 1600px) { .clyde-memory { max-width: 1100px; } }
/* ClydeBuild (2026-10-03): the interview banner, the write-up, Your builds. */
.clyde-build-banner {
  max-width: 760px; margin: 12px 0 0; padding: 12px 16px; border-radius: var(--r-sm);
  background: var(--paper-deep); border: 1px solid var(--alu-soft);
}
.clyde-build-banner p { margin: 0 0 8px; }
.clyde-build-banner .acts, .clyde-brief .acts { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.clyde-brief {
  max-width: 760px; margin-top: 28px; padding: 18px 20px; border-radius: var(--r-md);
  border: 1px solid var(--alu); background: var(--paper-raise);
}
.clyde-brief h2, .clyde-builds h2 { font-size: var(--t-lead); font-weight: var(--w-medium); margin: 0; }
.clyde-brief__body { margin: 12px 0 16px; }
.clyde-brief__edit {
  width: 100%; font: inherit; font-size: var(--t-body); padding: 8px 10px;
  border: 1px solid var(--alu); border-radius: var(--r-sm); background: var(--paper); resize: vertical;
}
.clyde-brief-parked { max-width: 760px; margin-top: 16px; }
.clyde-route { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--alu-soft); }
.clyde-route h3 { font-size: var(--t-body); font-weight: var(--w-medium); margin: 0 0 6px; }
.clyde-route__advice { margin: 0 0 12px; font-style: italic; color: var(--ink); }
.clyde-route__choices { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.clyde-route__choice { padding: 12px; border: 1px solid var(--alu-soft); border-radius: var(--r-sm); }
.clyde-route__choice p { margin: 8px 0 0; font-size: var(--t-small); color: var(--ink-60); }
.clyde-route__choice p b { color: var(--ink); }
@media (max-width: 720px) { .clyde-route__choices { grid-template-columns: 1fr; } }
.clyde-builds { max-width: 760px; margin-top: 44px; }
/* `.setrow .info span { display: block }` would put the dot on its own line. */
.setrow .info .clyde-build-row__state { display: flex; gap: 6px; align-items: center; font-weight: var(--w-medium); color: var(--ink); font-size: var(--t-small); }
.setrow .info .clyde-build-row__state .dot { display: inline-block; margin: 0; flex: none; }
.ask-foot[hidden] { display: none; }
.clyde-build-row__note { margin: 4px 0; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.clyde-build-row__answer { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; margin: 6px 0; }
.clyde-build-row__answer textarea {
  flex: 1 1 280px; font: inherit; font-size: var(--t-body); padding: 8px 10px;
  border: 1px solid var(--alu); border-radius: var(--r-sm); background: var(--paper-raise); resize: vertical;
}
@media (min-width: 1600px) { .clyde-brief, .clyde-builds, .clyde-build-banner { max-width: 1100px; } }
/* Files: who a shared folder is for — a plain select dressed as a chip. */
.share-who select {
  font: inherit; font-size: var(--t-caption); color: var(--ink-60); background: transparent;
  border: 1px solid var(--alu-soft); border-radius: var(--r-pill); padding: 3px 10px; margin-left: 4px;
}

/* ---- "Make an image" (image slice 1, 2026-10-03) ------------------------
   The button sits beside Ask and is the same height. A picture card is an
   answer like any other; the picture fills the column and never overflows
   it on a phone. A refusal or a busy card is plain text, not an alarm. */
.img-btn { flex: 0 0 auto; white-space: nowrap; }
.img-btn[hidden] { display: none; }
.img-card .body { min-height: 1.5em; }
.img-card__pic {
  display: block; max-width: 100%; height: auto; border-radius: var(--r-md);
  border: 1px solid var(--alu-soft); background: var(--paper);
}
.img-card__mark { margin: 0; font-size: var(--t-caption); }
/* "What I asked the picture AI" (2026-10-04) — the rewritten prompt, quietly. */
.img-card__asked { margin: 6px 0 0; font-size: var(--t-caption); }
.img-card--no .body { color: var(--ink-60); }
@media (max-width: 520px) { .img-btn { padding-left: 10px; padding-right: 10px; font-size: var(--t-small); } }

/* Founder 2026-10-04: the appearance button says which mode it is in. */
#theme-btn { width: auto; gap: 6px; padding: 0 10px; display: inline-flex; align-items: center; }
#theme-btn .theme-word { font-size: var(--t-caption); font-weight: var(--w-medium); color: var(--ink-60, currentColor); white-space: nowrap; }
@media (max-width: 600px) { #theme-btn .theme-word { display: none; } }

/* ⭐ WHICH BOX MAKES PICTURES (founder, 2026-10-04 13:39). One badge per
   card, spanning the caps grid. Same tokens as the rest of the card: the
   media box in the LOCAL green it already uses for a verified fact, none in
   ink, and could-not-ask in amber — the third state is a word, never a blank. */
.node-badges { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; }
.node-badge,
.setrow.peer .info .node-badge {
  display: inline-flex; align-items: center; margin-top: 0;
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  padding: 3px 9px; border-radius: var(--r-pill); border: 1px solid var(--alu-soft); color: var(--ink-60);
}
.node-badge.media,
.setrow.peer .info .node-badge.media { color: var(--local); border-color: var(--local); }
.node-badge.unlooked,
.setrow.peer .info .node-badge.unlooked { color: var(--amber); border-color: var(--amber); }
.v2-pictures { max-width: 72ch; }
.v2-pictures-facts span.k {
  font-size: var(--t-caption); letter-spacing: normal; text-transform: none;
  color: var(--ink-40); margin-top: 3px; white-space: nowrap;
}
.v2-pictures-facts span.v { font-size: var(--t-small); color: var(--ink); }
.v2-pictures-facts span.v.unlooked { color: var(--amber); }
.img-why__go { margin-left: 6px; }
