*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; width: 100%; overflow: hidden; }
body { font-family: var(--font); background: var(--bg-primary); color: var(--text-primary); font-size: var(--fs-base); line-height: 1.5; transition: background 0.2s, color 0.2s; -webkit-font-smoothing: antialiased; }
/* A palette may lay a wash over the frame -- Lollypop's multicolour glow is one
   -- by declaring --body-wash. It is fixed, so it sits still behind the panels
   as they scroll, and the panels themselves are opaque, so a wash never touches
   the legibility of the words on top. Declared as a token rather than as a rule
   naming one palette, because a rule naming a palette is a rule every palette
   added afterwards silently misses. */
body { background-image: var(--body-wash, none); background-attachment: fixed; }
button, input, textarea, select { font-family: inherit; font-size: inherit; color: inherit; }
button { cursor: pointer; }
svg { display: block; }
.ic { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.75; fill: none; stroke-linecap: round; stroke-linejoin: round; flex: none; }

/* ── The small plates ────────────────────────────────────────────
   The rail heads' `+`, the graph latch, the panel closers, the agent brakes and
   a triggered action's way in are all the same idea: one glyph on a small
   square. They wore a rotated diamond for one release, borrowed from the
   splash page's mark. It came back off because a diamond inscribed in a box
   has HALF the box's area, so the box had to grow from 22px to 30px to keep
   the glyph off the edges -- and 8px on every one of these, in a rail head and
   a top bar, is real estate this app has none of to give. The mark stays on
   the splash page, where there is room for it.

   The shape and paint are each button's own, below; what stays shared is the
   focus ring, because it must be a real `outline` ON THE BUTTON.
   `verify_a11y_keyboard` proves a control is focusable by snapshotting the
   element's own outline and shadow and requiring them to change, and its
   self-test strips `.addbtn`'s outline BY NAME to prove the census works. A
   ring drawn on a pseudo-element would be a control that census calls
   ringless, and a ring made of a background or border change would leave that
   self-test unable to go red. So: an outline, on the button, and no
   `border-radius` of its own -- an outline follows the corner the button is
   already cut with, which is the square's. */
.addbtn:focus-visible, .agc:focus-visible, .trig-btn:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* ── Copy an identifier ──────────────────────────────────────────
   One button, wherever a machine-readable string is on show: the build id in
   the rail, the account fingerprint, a device id, a build in the version
   history. It is deliberately quiet -- these sit beside values, not on top of
   them -- and comes up to full strength on hover, on focus, and whenever the
   pointer is anywhere on the line it belongs to.
   `align-self: center` because several of its hosts are baseline-aligned rows:
   a 20px box hung by a baseline overshoots the line and takes the row's height
   with it, which is the same fault `.device-rename` had.
   The 20px box sits UNDER the 24px target floor, and does so knowingly: these
   are secondary controls beside a value, the touch surfaces of the app are
   sized in mobile.css, and a 24px square in the 220px rail crowds the very
   string it is there to serve.
   The tick swaps in place on the same 24-unit grid, so a confirmation never
   moves what is beside it. */
.copy-id {
	flex: none; align-self: center;
	display: inline-flex; align-items: center; justify-content: center;
	width: 20px; height: 20px; padding: 0; margin-left: 2px;
	border: 0; border-radius: 5px; background: transparent;
	color: var(--text-muted); line-height: 0; cursor: pointer;
	opacity: .6; transition: opacity 0.12s, color 0.12s;
	/* For the hosts that are not flex rows -- the Models panel's footer is a
	   block of text -- so the box rides the line rather than hanging off its
	   baseline and stretching it. Ignored inside a flex container. */
	vertical-align: middle;
}
.copy-id .ic { width: 13px; height: 13px; }
.copy-id:hover, .copy-id:focus-visible { opacity: 1; color: var(--text-primary); background: var(--bg-hover); }
/* The row a copy button sits on brings it up as the pointer arrives, so a
   column of them does not compete with the values they belong to. */
.astat-copyrow:hover .copy-id,
.device-row:hover .copy-id,
.account-fp:hover .copy-id,
.models-default:hover .copy-id,
.rel-row:hover .copy-id,
.rel-build:hover .copy-id { opacity: 1; }
/* Done, and not done. The colour is the confirmation; nothing moves. */
.copy-id.copied { opacity: 1; color: var(--ok); }
.copy-id.failed { opacity: 1; color: var(--danger); }
.copy-id:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Login */
.login-screen { display: flex; align-items: center; justify-content: center; height: 100vh; background: var(--bg-primary); }
.login-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 32px; width: 360px; max-width: 90vw; }
.login-logo { height: 64px; margin: 0 auto 20px; display: block; }
.login-card form { display: flex; flex-direction: column; gap: 12px; }
.login-card input { background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 10px 14px; font-size: var(--fs-lg); }
.login-card input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.login-card input::placeholder { color: var(--text-muted); }
.login-card button { background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 10px 16px; font-size: var(--fs-lg); }
.login-card button:hover { background: var(--accent-hover); }
.login-error { color: var(--danger); font-size: var(--fs-sm); margin-top: 8px; min-height: 18px; }
.login-card a { display: block; text-align: center; margin-top: 16px; color: var(--accent-text); text-decoration: none; font-size: var(--fs-base); }

/* Said to a screen reader and to nothing else. The app's own name is drawn as a
   wordmark, which is an image and cannot be a heading, so the document had no
   <h1> at all and heading navigation started at level 2 with no level 1 above it.
   Clipped rather than `display: none`, which would take it out of the tree along
   with the pixels. */
.vh {
	position: absolute;
	width: 1px; height: 1px;
	margin: -1px; padding: 0; border: 0;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
}

/* ── App frame: topbar + four panels ─────────────────────────── */
/* The safe-area insets are added to the frame's own padding, not substituted for
   it: installed on a home screen there is no browser chrome holding the notch and
   the home indicator off the layout, and the top bar landed under the status bar.
   They are all zero in a browser tab, so this is the same padding it always was. */
#app { display: flex; flex-direction: column; height: 100vh; padding: calc(10px + var(--safe-t)) calc(12px + var(--safe-r)) calc(12px + var(--safe-b)) calc(12px + var(--safe-l)); gap: 8px; }

.topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 4px; }
.brand { display: flex; align-items: center; text-decoration: none; transition: opacity 0.15s ease; }
.brand:hover { opacity: 0.82; }
/* The integrated wordmark (mark + word as one vector), the same asset the unlock
   screen uses, in place of the mark plus a UI-font "Daimond". Two variants swap
   by theme because the topbar sits on the theme background: white lettering on
   the dark theme, dark grey on the light and lollypop themes. */
.brand-wordmark { height: calc(var(--logo-h) * 1.5); width: auto; display: block; }
/* The brand group. The wordmark was one of two marks here; the maker's badge has
   since moved to the other end of the bar, so this is now a group of one -- kept
   as a group because the bar distributes its children with space-between, and a
   wordmark that is a direct child of that gets a different left edge. */
.brand-group { display: flex; align-items: center; flex: none; min-width: 0; }
/* The maker's badge no longer lives in this row -- it is in the About dialog,
   which the button in its old slot opens. Its rules are down with the dialog's,
   because that is the only place it is drawn now. */
.brand-wordmark.wm-on-light { display: none; }
:root[data-ink="dark"] .brand-wordmark.wm-on-dark { display: none; }
:root[data-ink="dark"] .brand-wordmark.wm-on-light { display: block; }
.top-meter { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); overflow: hidden; white-space: nowrap; }
.top-meter .sep { color: var(--text-muted); }
/* NOTHING IN THIS GROUP MAY MOVE ITS NEIGHBOURS. The bar distributes its three
   children with space-between, so while this box shrank to its contents it sat
   flush right and every pixel that appeared inside it pushed the chip row left:
   measured at 1440px, a "Syncing..." pill arriving moved all thirteen chips
   86px, the pairing button another 122, and a held-back sweep 98. That is a row
   of targets that jumps while a thumb is on its way to one.
   `flex: 1 1 auto` here, and on `.panel-tags` below, fixes both ends instead:
   the row starts where the wordmark leaves off and the icons end at the bar's
   own edge, whatever is or is not between them. `justify-content: flex-end`
   is for the phone, where the chip row is not in this box at all (it is the
   footer there) and the icons would otherwise pack to the left. */
.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; min-width: 0; flex: 1 1 auto; }

.icon-btn { display: flex; align-items: center; justify-content: center; background: none; border: none; color: var(--text-secondary); font-size: var(--fs-xl); padding: 5px 7px; border-radius: var(--radius-sm); }
.icon-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.icon-btn.on { color: var(--accent); }
.icon-btn.dim { opacity: 0.4; }
/* One icon size across the top bar. The update chip and the appearance menu
   already drew at 18px while the guide "?" and the pairing glyph took the
   .icon-btn default of 16, so the row of five read as two different sets. */
.top-actions .icon-btn .ic { width: 18px; height: 18px; }


.main { display: flex; flex: 1; min-height: 0; }

/* Panels. The rail carries an inline width the user can pull; a dock panel
   fills its column, and the stage's occupants are sized by the engine. */
/* Scrollbars, everywhere. The browser's own paints a stepper arrow at each end
 * and a track in a colour of its choosing, which lands as a pale strip down the
 * side of a dark panel. Keep the width — it is comfortable to grab — and drop
 * the rest: no arrows, and a track that is simply the surface behind it, so the
 * only thing drawn is the thumb. */
::-webkit-scrollbar { width: 14px; height: 14px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
	background: var(--border-2);
	border-radius: 999px;
	border: 4px solid transparent;	/* Inset, so the thumb floats rather than fills. */
	background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: content-box; }

/* Firefox has no ::-webkit-scrollbar, so it is given the standard properties
 * instead. They are withheld from engines that do have it: Chromium honours
 * them, but the moment it does it stops honouring the rules above — including
 * the one that removes the arrows — and paints its own stepper buttons back. */
@supports not selector(::-webkit-scrollbar) {
	* { scrollbar-width: auto; scrollbar-color: var(--border-2) transparent; }
}

.panel { min-height: 0; }
/* Spending belongs in this list with its dock siblings. Left out, it stayed a
   block -- which reads correctly only for as long as nothing sets `display:
   flex` on it. The phone sheet does exactly that (`#msheet .panel`), and the
   panel's head and its body then laid out side by SIDE: a stranded "SPENDING"
   column on the left and the figures squeezed into what was left. */
/* Social belongs here for the same reason, and was left out in the same way.
   As a block it made `.imp-view { flex: 1 }` and `.imp-notes { flex: 1;
   overflow-y: auto }` (css/improve.css) both inert: a flex child of nothing is
   a block that takes its content's height, so `overflow-y: auto` never had an
   overflow to engage on. Measured 2026-08-28 with twenty notes kept: the panel
   was clipped at 434px and the list ran to 2606px, so NOT ONE note was on the
   screen and nothing would scroll to them. */
.rail, .agents, .work, .mail, .spend, .social { flex: none; display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
/* The `gap: 6px` above falls between the head and the body and NOWHERE ELSE
   here, which is why this panel needs no correction for it: every other child
   of `.social` is an `.imp-view`, and `.imp-view[hidden]` is `display: none`,
   so exactly one of the five is ever in the flow. Measured after the change:
   the note box starts 6px lower than it did and nothing else moved. */
/* Now that the panel is a flex column, the view is its flexible child: its own
   `height: 100%` would otherwise sit BESIDE the head in the height budget and
   push its own last rows out under the clip. */
.spend > .spend-view { flex: 1 1 auto; min-height: 0; }
.rail { width: var(--rail-w); gap: 0; }
.panel.nt { transition: none; }
.panel.closed { display: none; }

/* Every stage panel is the same card: the AI, a page, a document, a message.
   NOT the whole list -- the Graph and the Terminal wear the same card, declared
   in the file each of them owns (graph.css, terminal.css) rather than here. A
   stage panel that appears on none of the three lists is a bare block on the
   stage's ground, and worse than bare: a block is not a flex column, so nothing
   inside it can be told to fill the height. */
.ai, .web, .doc, .msg, .compose, .tools { min-width: 0; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-lg); display: flex; flex-direction: column; overflow: hidden; }

/* Drag handles between panels. */
.phandle { width: 10px; flex: none; cursor: col-resize; position: relative; touch-action: none; }
.phandle::after { content: ""; position: absolute; top: 0; bottom: 0; left: 4px; width: 2px; border-radius: 2px; background: transparent; transition: background 0.15s; }
.phandle:hover::after, .phandle.dragging::after { background: var(--accent); }
.phandle.closed { display: none; }
body.dragging { cursor: col-resize; user-select: none; -webkit-user-select: none; }

/* Rail/agents/work section headers. */
.railhead { display: flex; align-items: center; gap: 6px; padding: 2px 4px; }
/* The Spending header wraps its controls in a span; without this they lay out
   as inline text rather than as the button row every other header has. */
.spend-actions { display: flex; align-items: center; gap: 6px; }
.railhead > span:first-child { flex: 1; font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); white-space: nowrap; }
.rail-count { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); white-space: nowrap; }

.addbtn { width: 22px; height: 22px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--bg-secondary); color: var(--text-secondary); font-size: var(--fs-sm); line-height: 1; }
.addbtn:hover { color: var(--text-primary); }
.addbtn:disabled { opacity: 0.4; cursor: default; }
.addbtn:disabled:hover { border-color: var(--border); color: var(--text-secondary); }
/* A rail-head button that LATCHES has to look latched. `#link-graph-btn` opens
   the Graph and closes it again, so it carries `aria-pressed` -- and a state a
   screen reader announces while the screen shows nothing is half a control:
   press it, the panel goes away, and the button looks exactly as it did. Same
   treatment as `.tile-dlg-level[aria-pressed="true"]`, the app's other latched
   button, so the two read as one idea. */
.addbtn[aria-pressed="true"] {
	background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text);
}
label.addbtn { cursor: pointer; }
.rail-note { font-size: var(--fs-xs); color: var(--text-muted); padding: 0 6px 4px; }

/* ── The PPTW: play, pause, traffic light ─────────────────────────
   Drawn identically wherever it appears -- at the head of the rail for the
   root of the tree, on each Diamond tile, on each chat tile, and (phase G and
   H) on each mailbox, folder and triggered action.

   The name is the specification (notes2.txt line 3): "Pause/Play/Traffic light
   widgets (PPTWs) for all spendable functions. Green = all spendable functions
   active, Amber = some are active, Red = all paused." Play, pause, then the
   light, on the right.

   THE LIGHT CARRIES NOTHING BUT A COLOUR. It is a lamp, not an icon: no glyph,
   no symbol, no shape inside it. A glyph in the light is a verb worn as a noun,
   which is the fault the compact single button had -- a running node drew a
   green PLAY triangle, so the shape said "play" while the only thing pressing
   it could do was pause. Drawing that glyph inside the lamp puts the same
   confusion one place to the right.

   The light is not a button and takes no hover, no focus ring and no pointer
   cursor. Amber is derived and can never be set -- with nothing to press, that
   is true by construction rather than by convention. */
.pptw {
	flex: none; display: inline-flex; align-items: center; gap: 3px;
	line-height: 0;
	/* NOT the tile's pointer, which is inherited. A tile says "press me and I
	   open"; this strip does not open anything, and the light is not pressable
	   at all. It also keeps the light out of the keyboard census: an element
	   drawn as clickable that no key can reach is a defect by definition, and
	   the light's whole point is that it is not clickable. */
	cursor: default;
}
/* 13px, not the buttons' 20: a lamp beside two glyphs has to read as a
   different KIND of thing, and size is what says so when there is no shape
   inside it to say it. A ring at 12% of the colour lifts it off a tile whose
   ground happens to sit near the lamp's own hue. */
.pptw-lamp {
	flex: none; width: 13px; height: 13px; border-radius: 50%;
	background: var(--text-muted);
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-muted) 12%, transparent);
}
.pptw[data-state="play"]  .pptw-lamp {
	background: var(--ok);
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--ok) 12%, transparent);
}
.pptw[data-state="pause"] .pptw-lamp {
	background: var(--danger);
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--danger) 12%, transparent);
}
.pptw[data-state="mixed"] .pptw-lamp {
	background: var(--warn);
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 12%, transparent);
}

/* The two verbs. Both are always present and the inapplicable one is DISABLED
   rather than hidden, so a row never reflows under the thumb and pause is
   always in the same place. On an amber branch both are live -- which is the
   case one button had to guess at. */
.pptw-act {
	flex: none; width: 20px; height: 20px; padding: 0;
	display: flex; align-items: center; justify-content: center;
	border: none; border-radius: var(--radius-sm); background: none;
	line-height: 0; color: var(--text-secondary); cursor: pointer;
}
.pptw-act:disabled { cursor: default; }
.pptw-act:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-hover); }
.pptw-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Dimmed, not gone. A disabled verb still says what the control can do; it is
   the state that says why it cannot be done now. */
.pptw-act:disabled { color: var(--text-muted); opacity: 0.32; }

/* Centred by the flex box above and by the viewBox: the ink sits at 8,8 of
   0 0 16 16, so it is on the box's centre and not merely the sprite. */
.pptw-act .pptw-ic { width: 12px; height: 12px; display: block; fill: currentColor; }

/* The global control's own row, above the Diamonds list.
   Deliberately NOT a .railhead and deliberately not dressed as one. Given the
   section-header look, "EVERYTHING" sat a light's width to the right of
   "DIAMONDS" and "CHATS" and read as a third section that had lost its
   alignment. It is not a section, it is a switch: sentence case, a rule under
   it, and the light leading the way it leads on every tile below. The row's
   leading ink starts on the same 4px as the section labels, so the two kinds
   of row share a left edge without pretending to be the same kind.

   It also carries the rail's closer, at the far end. The cross used to sit on
   the Diamonds head below, where a cross at the end of a row named "Diamonds"
   reads as the closer of that section and was in fact the closer of the whole
   rail. This row is the one that speaks for all of it, so the closer and the
   pause now have the same scope and sit on the same line. The 4px right padding
   is the same 4px the section headers use, so the cross keeps a right edge with
   the buttons on the row below. */
.pptw-head {
	display: flex; align-items: center; gap: 6px;
	padding: 4px 4px 6px; border-bottom: 1px solid var(--border);
}
.pptw-slot { display: flex; flex: none; }
/* Ellipsised rather than nowrap alone: `flex: 1` gives it a zero basis but an
   AUTO minimum, so a longer translation of "Everything" would refuse to shrink
   and shove the closer off the end of the row. */
.pptw-head-label {
	flex: 1; min-width: 0; font-size: var(--fs-sm); font-weight: 500;
	color: var(--text-secondary); white-space: nowrap;
	overflow: hidden; text-overflow: ellipsis;
}
/* The closer holds its 22px against a long label. */
.pptw-head .panel-close { flex: none; }

.user-info { flex: 1; font-size: var(--fs-sm); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 4px; }
#logout-btn:hover { color: var(--danger); }

/* ── The rail: work above, the Admin panel below ─────────────────
   The divider moves; it does not go away. */
.rail-top { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; min-height: 0; overflow: hidden; }
.admin { display: flex; flex-direction: column; gap: 6px; flex: none; position: relative; }

/* The horizontal twin of .phandle. Its line is the divider, so nothing below
   needs a border of its own. */
.rhandle { height: 10px; flex: none; cursor: row-resize; position: relative; touch-action: none; }
.rhandle::after { content: ""; position: absolute; left: 0; right: 0; top: 4px; height: 2px; border-radius: 2px; background: var(--border); transition: background 0.15s; }
.rhandle:hover::after, .rhandle.dragging::after { background: var(--accent); }
body.resizing-v { cursor: row-resize; user-select: none; -webkit-user-select: none; }

/* ── The status header ───────────────────────────────────────────
   One row per question the user would otherwise have to open something to
   answer. A row that can be acted on is a button that goes there. */
/* The Status strip: fixed, pinned to the rail bottom, always on show. */
.admin-status { display: flex; flex-direction: column; gap: 1px; flex: none; border-top: 1px solid var(--border); padding-top: 6px; }
.astat-row { display: flex; align-items: center; gap: 6px; padding: 3px 4px; border-radius: var(--radius-sm); font-size: var(--fs-xs); color: var(--text-secondary); min-width: 0; }
.astat-row.astat-id { gap: 4px; padding: 0; }
/* A status row that carries a copy button beside it. The row stays the button
   it always was -- it opens what it names -- and the copy sits outside it,
   because a button inside a button is neither valid nor clickable. */
/* The wrapper takes the RIGHT padding its sibling rows carry on `.astat-row`.
   Without it the copy button ends flush with the panel's edge -- measured at a
   0px gap where every other row stands 4px off it -- and read as clipped. */
.astat-copyrow { display: flex; align-items: center; padding-right: 4px; }
.astat-copyrow > .astat-row { flex: 1 1 auto; min-width: 0; }
.astat-copyslot { flex: none; display: flex; align-items: center; }
.astat-btn { width: 100%; background: none; border: none; text-align: left; }
.astat-btn:hover { background: var(--bg-hover); }
.astat-label { flex: none; color: var(--text-muted); }
.astat-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.astat-aside { flex: none; font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary); white-space: nowrap; }
/* The dot carries the state, so the row reads at a glance without being read. */
.astat-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); }
.astat-dot.ok { background: var(--ok); }
.astat-dot.warn { background: var(--danger); }
.astat-dot.off { background: var(--border-2); }
.astat-lock { flex: none; font-size: var(--fs-3xs); color: var(--text-muted); }

/* The sync row. Its chip (`#sync-chip`, built by js/sync.js) and its resting
   line share one row and exactly one of them is ever showing, so the row's
   height never changes and nothing in the strip moves under a thumb.
   These rules were injected by sync.js while the chip was a pill in the top
   bar; they are here now because the row they draw is in the markup, and
   because this is where the strip's other ten rows are drawn. */
.astat-sync { position: relative; }
#sync-chip {
	display: none;
	flex: 1 1 auto;
	align-items: center;
	gap: 6px;
	min-width: 0;
	color: var(--text-secondary);
}
#sync-chip .stext { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sync-chip[data-state="syncing"] { color: var(--accent); }
#sync-chip[data-state="synced"]  { color: var(--ok); }
#sync-chip[data-state="off"]     { color: var(--text-secondary); cursor: pointer; }
/* A stall is not an error and not a success: something is wrong that the user
   can fix, which is what --warn is for in the rest of the app. */
#sync-chip[data-state="stalled"] { color: var(--warn); }
/* The dot is the strip's dot, taking its colour from the state above it rather
   than from a class, since the state is already on the element. */
#sync-chip .sdot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
#sync-chip[data-state="syncing"] .sdot { animation: syncpulse 1s ease-in-out infinite; }
@keyframes syncpulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
	#sync-chip[data-state="syncing"] .sdot { animation: none; opacity: 1; }
}

/* The setup prompt. Until Daimond has a model it cannot answer anything, so the model row is
   the one thing to do — it glows gently, on a slow cycle so it draws the eye without nagging.
   Respects a user who has asked the OS for less motion. */
@keyframes astat-pulse {
	0%, 100% { background: var(--bg-hover); box-shadow: 0 0 0 0 rgba(210, 96, 79, 0.0); }
	50%      { background: var(--accent-soft); box-shadow: 0 0 0 2px rgba(210, 96, 79, 0.28); }
}
/* The Pro row reads its value as a link when there is an upgrade to take, so
   "Upgrade to Pro" invites a click the way a bare status figure would not. */
.astat-row.astat-pro-upgrade .astat-val { color: var(--accent-text); text-decoration: underline; }
.astat-row.astat-pro-upgrade:hover .astat-val { filter: brightness(1.1); }
.astat-row.astat-pulse { animation: astat-pulse 2.2s ease-in-out infinite; }
.astat-row.astat-pulse .astat-val { color: var(--text-primary); font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
	.astat-row.astat-pulse { animation: none; background: var(--accent-soft); box-shadow: 0 0 0 1px rgba(210, 96, 79, 0.4); }
}

/* The Admin panel: an overlay drawer that rises above the Status strip when
   summoned and is gone again when finished. Hidden until #admin has .admin-open. */
.admin-body {
	position: absolute;
	/* Inset from the rail's edges rather than flush with them: the rail clips
	   its overflow, so a drawer at left:0/right:0 has its side shadow cut away
	   and only the top and bottom survive. */
	left: 5px; right: 5px;
	bottom: 100%;
	margin-bottom: 6px;
	display: none;
	flex-direction: column;
	/* BOUNDED BY THE RAIL, NOT BY A NUMBER. This was `min(70vh, 540px)`, and 540
	   is the room above the Status strip on the window it was written on -- so on
	   any shorter one the drawer grew straight past the rail's top edge, and the
	   rail clips its overflow, so the drawer's own head went with it. Hit-tested
	   on 2026-08-28, sweeping the window from 640px to 1200px: at 840px and below
	   the drawer's × was not on the screen at all, and this is the one surface in
	   the app that covers the whole rail and has no other way out. At 640px the
	   drawer stood 157px above the rail's top.
	   `--admin-room` is that distance, measured by `boundDrawer` in js/daimond.js
	   whenever the rail or the strip changes size. It is also what makes the
	   strip's own height safe to change at all: a taller strip now makes the
	   drawer SHORTER instead of pushing it up into the rail's head, which is what
	   `#new-diamond-btn` was 14px from when the spend row was made permanent.

	   THE COMFORT CAPS ARE GONE WITH IT. `70vh` and `540px` both put the top edge
	   wherever the arithmetic landed, and at 940px of window that was inside
	   `#new-diamond-btn` -- half the button covered, its centre not, which is a
	   control that looks pressable and is not. One bound, at a structural
	   boundary, is what makes the top edge the same place at every height. */
	max-height: var(--admin-room, 540px);
	background: var(--bg-secondary);
	border: 1px solid var(--border);
	border-radius: 12px;
	box-shadow: 0 8px 28px rgba(0, 0, 0, 0.22);
	overflow: hidden;
	z-index: 30;
}
.admin.admin-open .admin-body { display: flex; }
/* The drawer's own header: the contextual title, and the one control that
   dismisses it. Hidden in form mode, where the form supplies its own head. */
.admin-drawer-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 8px 6px 8px 10px; border-bottom: 1px solid var(--border); }
.admin-drawer-head .admin-title { font-size: var(--fs-sm); }
.admin.admin-form-mode .admin-drawer-head { display: none; }
.admin-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 8px; }
.admin-scroll::-webkit-scrollbar { width: 8px; }
.admin-scroll::-webkit-scrollbar-track { background: transparent; }
.admin-scroll::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 999px; }
.admin-body .settings-section:last-child { margin-bottom: 0; }
/* The rail no longer resizes its two panes: the Status strip is fixed and the
   Admin panel overlays, so the drag handle has nothing to size. */
#handle-split { display: none; }
/* The rail is not a modal card: four money buttons in a column this narrow have
   to wrap rather than shrink to an unreadable width. */
.admin-body .credits-packs { flex-wrap: wrap; }
.admin-body .credit-pack { flex: 1 1 calc(50% - 4px); }

/* A view in the Admin body: the same furniture as a dialog, without the backdrop. */
.admin-view-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 8px; }
/* In the modal the hosted view names itself. The card used to carry a static
   "Settings" heading instead, which was wrong the moment anything but the
   settings view was hosted -- "Settings" sat over an "Add a mailbox" form. The
   head travels with the view, so it is always the right words, and it wears the
   card's heading size rather than the drawer's. */
#settings-slot .admin-view-head { margin-bottom: 16px; }
#settings-slot .admin-title { font-size: var(--fs-2xl); }
#settings-slot .admin-back { font-size: var(--fs-2xl); padding: 2px 10px; }
/* A built form brings a head of its own, which the slot's head already repeats
   -- "Add a mailbox" twice, one above the other. The slot's own head wins: it is
   the card's heading, and its × is wired to the form's cancel, so the promise
   the caller is waiting on is still answered. */
#settings-slot .admin-view .admin-view-head { display: none; }
/* One way out, not three. The head's × closes the card (and cancels a form
   properly); the trailing Close underneath said the same thing less well, and
   sat below the fold of a long form. The phone has always hidden it. */
#settings-modal #settings-close { display: none; }
.admin-title { font-size: var(--fs-base); font-weight: 600; color: var(--text-primary); }
.admin-back { background: none; border: none; color: var(--text-muted); font-size: var(--fs-lg); line-height: 1; padding: 2px 6px; border-radius: var(--radius-sm); }
.admin-back:hover { background: var(--bg-hover); color: var(--text-primary); }

/* Home: the panel at rest. A call to action while Daimond cannot run, and the
   account's own controls — which used to be a floating menu, and are one fewer
   popup now that there is a panel to put them in. */
.admin-cta { width: 100%; background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 9px 12px; font-size: var(--fs-sm); margin-bottom: 10px; }
.admin-cta:hover { background: var(--accent-hover); }
/* ONE left edge for the whole drawer: 8px, which is where the rows land, and
   the rows are what there are most of. Home stacks six kinds of line in one
   narrow column -- section header, fingerprint, row, device, note, link -- and
   each had arrived with its own horizontal padding, so the words started at
   4, 6, 8 and 15px down a 190px column. Nothing there is centred and nothing is
   ragged on purpose, so the eye reads the drift as four columns.
   The CTA is left out: its label is centred in a filled button, so its padding
   sets a width, not a margin. */
.admin-sec { font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); padding: 2px 8px 4px; }
.admin-item { display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text-primary); font-size: var(--fs-sm); padding: 7px 8px; border-radius: var(--radius-sm); }
.admin-item:hover { background: var(--bg-hover); }
/* Keyboard focus, themed, matching the app's ring elsewhere (workspace.css).
   These rows take real focus when a dialog closes and hands it back, and a
   borderless button on a borderless panel showed nothing at all -- the caret was
   somewhere in a list of eight identical lines. The accent is defined in all
   three palettes, so the ring survives the light themes; a dark-only colour
   would not. The offset stays inside the scroller's 8px padding. */
/* The controls that had no themed ring of their own fell back to Chromium's,
   which is drawn in rgb(16, 16, 16) -- a black ring on a near-black panel, and
   clipped 1px besides by the scrollers they sit in. The offset is NEGATIVE
   here, so the ring is painted inside the control's own box and no ancestor's
   overflow can take it. */
#user-row:focus-visible,
#settings-btn:focus-visible,
#spend-row:focus-visible,
.astat-row:focus-visible,
.models-prov-head:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: -2px;
	border-radius: var(--radius-sm);
}

.admin-item:focus-visible {
	outline: 2px solid var(--accent);
	/* INSET, so the ring cannot be cut off. Drawn 2px OUTSIDE, the last item in
	   the drawer needed 4px the container did not have -- measured at -0.6px of
	   room, with overflow hidden and nothing to scroll -- so a keyboard user lost
	   the bottom of the ring on the one control at the edge, where it is hardest
	   to guess where you are. An inset ring is bounded by the button itself and
	   is therefore clip-proof at every spacing and in every palette. */
	outline-offset: -2px;
	background: var(--bg-hover);
}
.admin-item.danger { color: var(--danger); }
.admin-item.danger:hover { background: var(--danger); color: var(--on-fill, #fff); }
.admin-note { font-size: var(--fs-xs); color: var(--text-muted); padding: 4px 8px 8px; line-height: 1.5; }
/* One heading idiom for the whole drawer. The account (user-name) menu heads its
   groups with `.admin-sec` -- small, upper-case, muted -- while the Models,
   Credits and Push views wore `.settings-section h3` at `--fs-lg`, so the two
   menus read as two different systems. Inside the drawer the h3 takes the
   `.admin-sec` metrics; the global `.settings-section h3` (below) still serves
   the identity modal and anywhere else off the drawer. No left padding here: the
   section already supplies the drawer's 8px, so the words land on the same edge
   as every `.admin-sec` and `.admin-item`. */
.admin-body .settings-section h3 {
	font-size: var(--fs-2xs);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--text-muted);
	margin: 0;
	padding: 2px 0 6px;
}
/* The way through to the operator console. Drawn as one of the drawer's own
   rows rather than as a button: it leads somewhere, it does not do anything.
   So it spans the width they span and starts its words where they start theirs.
   It is the one row here with a border, so it pays for that out of its padding:
   7px of padding plus 1px of border is the 8px the borderless rows set. */
.admin-console-link {
	display: block;
	margin: 2px 0;
	padding: 6px 7px;
	background: var(--bg-tertiary);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm);
	color: var(--text-primary);
	font-size: var(--fs-sm);
	text-decoration: none;
}
.admin-console-link:hover { border-color: var(--accent); color: var(--accent-text); }

/* ── Chats list (sessions) ───────────────────────────────────── */
/* 4px of side padding, which is what `.railhead` and `.pptw-head` above it
   already use. The two lists disagreed -- Diamonds sat on 6px and Chats on 0 --
   so a Diamond tile's card was 6px narrower than a chat tile's and their two
   columns of pause lights were 6px apart down the rail. On 4px every leading
   edge in the rail starts in the same place: the global light, the section
   labels, and both kinds of tile. */
.session-list { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; gap: 6px; padding: 0 4px 2px; }

.new-session-panel { display: flex; gap: 6px; padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border-2); border-radius: var(--radius); flex: none; }
.new-session-model { flex: 1; min-width: 0; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 6px 8px; font-size: var(--fs-sm); }
.new-session-model:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.new-session-start { background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 6px 12px; font-size: var(--fs-sm); white-space: nowrap; }
.new-session-start:hover { background: var(--accent-hover); }

.session-box { padding: 8px 9px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-secondary); cursor: pointer; flex: none; }
.session-box:hover { border-color: var(--border-2); }
.session-box.active { border-color: var(--accent); background: var(--accent-soft); }
.session-box-header { display: flex; align-items: center; gap: 6px; }
.session-box-name { flex: 1; font-size: var(--fs-sm); font-weight: 500; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-box.active .session-box-name { color: var(--accent-text); }
.session-box-rename { flex: 1; min-width: 0; background: var(--bg-primary); border: 1px solid var(--accent); border-radius: var(--radius-sm); color: var(--text-primary); padding: 2px 6px; font-size: var(--fs-sm); }
.session-box-rename:focus { outline: none; }
/* The cog, and the closer cross notes4 asked back beside it. Both always
   visible, never on hover: the old cross was `opacity: 0` until the pointer
   arrived, which is no control at all on a phone -- and it put the one
   irreversible act on the tile's easiest pixel. 20px square each, the same
   box the pause light opposite them wears, so the row sits on one line and
   one baseline.
   The cross sits OUTSIDE the cog (rightmost in source order, which this
   flex row draws left to right) -- the same slot every other closer in the
   app holds, from a panel head's `.panel-close` to the tile dialog's own
   `x`. A hand that has learned "the × is at the row's far edge" finds it
   there on a tile too; the cog, one step in, is for settings that are not
   final. */
.tile-cog, .tile-x {
	flex: none; width: 20px; height: 20px; padding: 0;
	display: flex; align-items: center; justify-content: center;
	background: none; border: none; border-radius: var(--radius-sm);
	line-height: 0; color: var(--text-muted);
}
.tile-cog:hover { color: var(--text-primary); background: var(--bg-hover); }
.tile-cog:focus-visible, .tile-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Danger on hover, not at rest: the same treatment `.queue-x` wears, so a
   cross that ends something for good reads the same wherever it appears. */
.tile-x:hover { color: var(--danger); background: var(--bg-hover); }
.session-box-meta { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 3px; font-family: var(--font-mono); }
.session-box-ctx, .session-box-cost, .session-box-time { font-size: var(--fs-2xs); color: var(--text-muted); }

/* ── Simple and Max ──────────────────────────────────────────────
   EVERY RULE FOR THE TWO VIEWS LIVES IN THIS BLOCK. Put one beside its own
   component and nobody can answer "what does Simple hide?" without grepping,
   and the answer rots silently the first time a class is renamed.

   The two views are not "more" and "less". They differ in WHERE A CONTROL LIVES
   relative to the thing it controls:

     Simple  one object at a time, controls inside it -- open the tile, change
             it, close it.
     Max     many objects at once, controls hoisted onto the tile, so they can
             be COMPARED and changed without navigating.

   Comparison is the job Max exists for: "which of my six Diamonds is on the
   expensive model" cannot be answered by opening six dialogs. That also says
   what does NOT belong on a Max tile -- anything nobody compares and nobody
   changes mid-flow stays in the dialog however much room there is. Delete,
   rename, the triggered-action editor and scope are all in that group.

   Two invariants, both asserted in dev/verify_view.mjs:

     1. Simple hides INFORMATION, never AFFORDANCES. Everything reachable in Max
        is reachable in Simple within one more click, and the way in -- the cog
        -- is always visible. Hide the route as well as the detail and the
        reader concludes the app is broken.
     2. Simple never hides a warning, an error, or a SPENDING CONTROL. Notes2
        opens on the Transparent Control Principle; a display preference that
        hid a traffic light would contradict the app's own first rule.

   Hidden with CSS rather than by building a different tile, so every handler is
   wired either way -- a hidden pulldown still answers `DaimondModels.pick`, so
   Start on a Simple tile starts on the model the tile holds rather than on
   nothing. `display: none` also takes an element out of the tab order, so this
   costs nothing in keyboard terms.

   What Simple does NOT hide: the pause light, the name, the queue badge, the
   pending-fold badge and the tags. The first three are money or work in
   flight, and the last two are how a Diamond is found. */
.session-box[data-detail="simple"] .tile-model,
.session-box[data-detail="simple"] .tile-worker-row,
.session-box[data-detail="simple"] .tile-model-chip,
.session-box[data-detail="simple"] .tile-meter,
.session-box[data-detail="simple"] .session-box-ctx,
.session-box[data-detail="simple"] .session-box-time { display: none; }
/* A meta row whose every child is hidden still costs its top margin. Its own
   rule, because an unsupported `:has()` would otherwise drop the whole list
   above with it. */
.session-box[data-detail="simple"] .session-box-meta:not(:has(.diamond-pending)) {
	display: none;
}
/* Per-object spend is a Max thing and a new one: "which Diamond is eating the
   money" is comparable across objects and unanswerable anywhere else in the
   app. It is information, not a control, so hiding it in Simple does not touch
   invariant 2 -- the light and the global spend readout are both still there. */
.session-box[data-detail="simple"] .session-box-spend { display: none; }
.session-box-spend { font-size: var(--fs-2xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* The Diamond tile's meter row: context bar · relative time · spend, in the
   shape of the meter on an ordinary chat tile (`.tile-meter`) minus the token
   count. Shown in Max, hidden in Simple with the rest of the comparable
   detail -- it is information, not a control, so invariant 2 is untouched.
   A missing context bar means the daimon has spent no tokens or its model
   publishes no window; the row still carries time and spend, so it renders
   whenever either exists. */
.diamond-meter { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; margin-top: 3px; min-width: 0; font-family: var(--font-mono); font-size: var(--fs-3xs-2xs); color: var(--text-muted); }
.session-box[data-detail="simple"] .diamond-meter { display: none; }
/* Tags, on a row of their own. They used to sit inline in `session-box-meta`,
   which read as though they belonged to the version number and the model chip,
   and a heavily-filed Diamond pushed the rest of the row off the tile.

   NOT HIDDEN IN SIMPLE. The rule above says so in as many words -- "What Simple
   does NOT hide: ... the pending-fold badge and the tags ... the last two are
   how a Diamond is found" -- and moving them out of `meta` came within one line
   of silently reversing it: the row inherited nothing, so a `display: none` was
   written for it and nothing in the suite went red. Finding a Diamond by its tag
   is the one thing Simple must not take away, because Simple is the view with
   fewer other ways to tell two tiles apart.

   EVERY TAG RENDERS and the row SCROLLS SIDEWAYS rather than wrapping or
   truncating with a +N chip: an unrendered tag cannot be clicked into a filter,
   so the overflow chip hid the one thing a tag exists for. The row does not
   wrap, and `scrollbar-width: none` keeps a scrollbar from stealing a row of
   height on a narrow rail -- the chips say "more this way" by being cut at the
   edge, which is the affordance a horizontal clip gives for free. */
.session-box-tags {
	display: flex; flex-wrap: nowrap; gap: 4px; margin-top: 3px;
	overflow-x: auto; overflow-y: hidden;
	scrollbar-width: none;           /* Firefox */
	-ms-overflow-style: none;        /* legacy Edge */
}
.session-box-tags::-webkit-scrollbar { display: none; }   /* Chromium/Safari */
.session-box-tags .tag-chip { flex: none; }                /* keep chips their own width */

/* Simple is a DESIGN, not a subtraction. Take the room the hidden rows leave
   and give it to what is left, or the tile reads as the dense one with holes in
   it rather than as a calm one. Bigger target, fewer things in it. */
[data-view="simple"] .session-box[data-detail="simple"] {
	padding-top: 10px;
	padding-bottom: 10px;
}
[data-view="simple"] .session-box[data-detail="simple"] .session-box-name { font-size: var(--fs-md); }

/* THE STATUS HEADER IS NOT TRIMMED IN SIMPLE, and the reason is invariant 1.
   Simple hid three of its rows for a while -- Tools, Version and the machine
   workspace -- on the grounds that seven rows of mostly-static reassurance is
   the most intimidating thing on a first run. That was wrong, and wrong by the
   rule written above it: every one of those rows is a BUTTON. Version opens the
   release history and the transparency log; Tools opens the panel whose whole
   purpose is telling a user what this app can do, which they will never ask for
   if they do not know it exists. Hiding them removes the ROUTE, not the detail,
   which is the one thing Simple may never do.

   `verify_release` caught it by failing to click a row that had quietly gone. */

/* ── Center panel ────────────────────────────────────────────── */
.chead { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 9px 14px; border-bottom: 1px solid var(--border); min-height: var(--header-h); }
.ctitle { font-size: var(--fs-base); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ── The crystal face ────────────────────────────────────────────
   The AI panel has two faces: a conversation, and a Diamond's crystal. The
   crystal is a document the user owns, so the panel takes the mark beside the
   name and squares its corners -- everything else in the app is rounded, so
   square IS the distinction, in both skins (the warm skin only enlarges
   --radius-lg, which this beats on specificity). */
.chead-mark { width: 18px; height: 18px; flex: none; }
/* The title takes the slack, so the mark sits against it rather than being
   spread away from it by the head's space-between. */
.panel.ai .chead .ctitle { flex: 1 1 auto; min-width: 0; }
.panel.ai.crystal-face { border-radius: 0; }
.chead-right { display: flex; align-items: center; gap: 8px; flex: none; }
/* The two faces of a Diamond, as one segmented control rather than two buttons:
   they are one choice, and a pair of separate buttons reads as two actions. */
.dview { display: flex; flex: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.dview-btn {
	background: var(--bg-tertiary); border: 0; color: var(--text-muted);
	padding: 3px 10px; font-size: var(--fs-2xs); white-space: nowrap;
}
.dview-btn + .dview-btn { border-left: 1px solid var(--border); }
.dview-btn:hover { color: var(--text-primary); }
.dview-btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-text); }
.dview-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.cmeter { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary); white-space: nowrap; }
.chat-model-select { background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-secondary); font-size: var(--fs-xs); padding: 3px 6px; max-width: 170px; }
.chat-model-select:hover { border-color: var(--accent); color: var(--text-primary); }
/* A pulldown, so `:focus-visible` -- see the note at `.settings-select`. */
.chat-model-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }

.chat-output { flex: 1; overflow-y: auto; padding: 16px 20px; font-size: var(--fs-base); line-height: 1.6; color: var(--text-primary); word-break: break-word; }
.chat-input-bar { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); }
#chat-input { flex: 1; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius); color: var(--text-primary); padding: 9px 12px; font-size: var(--fs-base); resize: none; max-height: 120px; line-height: 1.5; }
#chat-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
#chat-input::placeholder { color: var(--text-muted); }
/* Send, and the walk back up your own questions. Both are square and exactly as
   tall as the input at rest — 21px of line + ~9px of vertical padding + 2px of
   border — so the row reads as one control rather than a box with taller buttons
   beside it. The bar aligns to flex-end, so they stay on the baseline as the
   input grows. */
#chat-send, #chat-jump, #chat-end {
	box-sizing: border-box;
	height: 32px;
	width: 32px;
	flex-shrink: 0;
	border-radius: var(--radius);
	font-size: var(--fs-base);
	display: flex;
	align-items: center;
	justify-content: center;
}
#chat-send { background: var(--accent); color: var(--on-fill, #fff); border: none; }
#chat-send:hover { background: var(--accent-hover); }
#chat-send:disabled { opacity: 0.4; cursor: default; }
#chat-send.stop { background: var(--danger); }
#chat-send.stop:hover { filter: brightness(1.1); }
#chat-jump, #chat-end { background: var(--bg-tertiary); border: 1px solid var(--border-strong); color: var(--text-secondary); }
/* The two jumps draw an SVG chevron now, not a text arrowhead, so `font-size` no
   longer decides how big the mark is -- the glyph does. Stated here so the touch
   size and the mark size are set in the same place and cannot drift apart. */
#chat-jump .ic, #chat-end .ic { width: 16px; height: 16px; }
#chat-jump:hover, #chat-end:hover { background: var(--bg-hover); color: var(--text-primary); }
/* Chat messages */
/* An answer belongs to the question above it, so it sits close to it; the air
   goes between turns instead, where it separates things that are separate.
   (Each message also carries 22px of bottom padding — the gutter the copy
   button lives in — so these margins are on top of that, not instead of it.) */
.chat-msg { margin-bottom: 6px; }
.chat-msg-user { margin-bottom: 2px; }
.chat-msg-user { text-align: right; }
.chat-msg-user .chat-msg-content {
	display: inline-block;
	background: var(--accent-soft);
	color: var(--accent-text);
	border-radius: var(--radius-lg);
	padding: 8px 14px;
	max-width: 82%;
	text-align: left;
}
.chat-msg-assistant .chat-msg-content { max-width: 92%; }
/* A turn the browser closed in the middle of: what arrived is shown, dimmed and badged, with a
   Continue button that runs it again. */
.chat-msg.interrupted .chat-msg-content { opacity: 0.85; }
.turn-interrupted { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; padding: 8px 10px; border: 1px dashed var(--warn); border-radius: var(--radius-sm); background: var(--warn-bg); }
.ti-label { font-size: var(--fs-xs); color: var(--warn); flex: 1; min-width: 0; }
.ti-continue { flex: none; background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); font-size: var(--fs-xs); padding: 5px 12px; }
.ti-continue:hover { background: var(--accent-hover); }
.chat-msg-content p { margin: 0 0 8px; }
.chat-msg-content p:last-child { margin-bottom: 0; }
.chat-msg-content pre { background: var(--bg-tertiary); border-radius: var(--radius-sm); padding: 12px; overflow-x: auto; font-size: var(--fs-sm); margin: 8px 0; }
.chat-msg-content code { font-family: var(--font-mono); font-size: var(--fs-sm); background: var(--bg-tertiary); padding: 2px 6px; border-radius: 3px; }
.chat-msg-content pre code { background: none; padding: 0; }
.chat-msg-content ul, .chat-msg-content ol { margin: 8px 0; padding-left: 24px; }
.chat-msg-content h1, .chat-msg-content h2, .chat-msg-content h3 { margin: 12px 0 6px; }

/* ── Queued messages ────────────────────────────────────────────
   What you typed while the answer was still arriving. It reads as a user
   message that has not happened yet: the same bubble on the same side, dashed
   and dimmed, under a line saying what it is waiting for. Deliberately NOT
   .chat-msg-user — that class is how a turn is counted. */
.chat-queued { margin-top: 10px; }
.chat-queued-head { font-size: var(--fs-2xs); color: var(--text-muted); text-align: right; margin-bottom: 4px; }
.chat-msg-queued { position: relative; text-align: right; margin-bottom: 4px; padding-right: 20px; }
.chat-msg-queued .chat-msg-content {
	display: inline-block;
	background: none;
	color: var(--text-secondary);
	border: 1px dashed var(--border-2);
	border-radius: var(--radius-lg);
	padding: 7px 13px;
	max-width: 82%;
	text-align: left;
	cursor: text;
}
.chat-msg-queued .chat-msg-content:hover { border-color: var(--accent); color: var(--text-primary); }
/* Withdraw a message that has not been sent. Centred on the BUBBLE: a queued
   message is still a .chat-msg, so it inherits the copy button's gutter even
   though nothing is ever drawn in it, and a plain 50% centred the closer on the
   bubble PLUS that gutter -- half a gutter low, level with the bubble's last
   line rather than with the bubble. */
.queue-x {
	position: absolute;
	top: calc((100% - var(--copy-gutter, 0px)) / 2);
	right: 0;
	transform: translateY(-50%);
	background: none;
	border: none;
	color: var(--text-muted);
	font-size: var(--fs-lg);
	line-height: 1;
	padding: 0 4px;
	cursor: pointer;
}
.queue-x:hover { color: var(--danger); }

/* The fold notice — Daimond's own edit of the conversation, drawn where it
   happened. Left-aligned and full width, unlike the right-aligned bubbles above:
   it is not something anyone SAID, it is a note about the thread itself, and
   giving it a speaker's shape would read as the model summarising its own work.
   Tokens throughout, because an inline style does not survive a palette. */
.chat-msg-compacted { margin: 10px 0; }
.chat-msg-compacted .chat-queued-head { text-align: left; }
.chat-msg-compacted .chat-msg-content {
	background: var(--bg-tertiary);
	color: var(--text-secondary);
	border: 1px dashed var(--border-2);
	border-left: 3px solid var(--border-2);
	border-radius: var(--radius-sm);
	padding: 8px 12px;
	font-size: var(--fs-xs);
	line-height: 1.5;
	white-space: pre-wrap;
}

/* THE FOLD IS A BOUNDARY, NOT A MESSAGE.
 *
 * A tester read a long chat as losing its history. Nothing was: the conversation had folded,
 * the app said so, and the sentence sat at the bottom of a transcript that still showed every
 * earlier message in full. See `appendCompacted` in www/js/daimond.js for the whole reasoning.
 *
 * So it is drawn as a rule across the thread. The rule is the signal; the ink above it is
 * NOT reduced, because dimming the words would be the app hiding a conversation it has just
 * been accused of losing. What marks them is a line down the side, in the same border the
 * notice wears, so the eye can follow the region back to the rule that names it. */
.chat-fold-line { position: relative; padding-top: 14px; }
.chat-fold-line::before {
	content: '';
	position: absolute;
	top: 4px; left: 0; right: 0;
	border-top: 1px dashed var(--border-2);
}
.chat-fold-boundary {
	margin-top: 6px;
	padding: 0 2px;
	color: var(--text-secondary);
	font-size: var(--fs-xs);
	line-height: 1.5;
}
.chat-above-fold {
	border-left: 2px solid var(--border-2);
	padding-left: 8px;
	margin-left: -10px;
}

/* How the turn ended — FURNITURE, and the styling is the whole of the
   not-crying-wolf rule (dev/CONTRACT_CLAIMS.md §3). One quiet line in the
   register of a timestamp: no box, no rule, no colour that claims a fault. An
   app that appends a warning to every turn teaches its reader to skip the one
   that mattered, and that is exactly the failure this line exists to prevent.

   `.ended-notice` is the promotion, and it is deliberately NOT the error or the
   alert styling — neither --danger nor --warn appears here. What changes is
   contrast and a thin rule: one step nearer the reader, which is what a notice
   is, without a colour that says something broke. `refused > 0`, `failed > 0`
   or a missing path is the only thing that reaches it. */
.chat-msg-ended {
	margin: 2px 0 12px;
	font-size: var(--fs-2xs);
	line-height: 1.5;
	color: var(--text-muted);
}
.chat-msg-ended .end-paths {
	margin-top: 2px;
	font-family: var(--font-mono, ui-monospace, monospace);
	word-break: break-all;
}
.chat-msg-ended.ended-notice {
	color: var(--text-secondary);
	border-left: 2px solid var(--border-strong);
	padding-left: 8px;
}
/* The same fact on a worker's tile, which is not a thread: it rides with the
   vision note, in the app's own voice rather than the worker's. */
.acard .anote.aended { color: var(--text-muted); }
.acard .anote.aended.ended-notice {
	color: var(--text-secondary);
	border-left: 2px solid var(--border-strong);
	padding-left: 6px;
}

/* Diamonds rail list — reuses .session-box styling via the shared class. A long
   list scrolls rather than squeezing the chats out of the pane above it. */
.diamond-list { display: flex; flex-direction: column; gap: 6px; padding: 0 4px 6px; flex: 0 1 auto; min-height: 0; overflow-y: auto; }

/* Find a Diamond: a search box that is always there, and the standing tag pool
   beneath it. */
.diamond-find { display: flex; flex-direction: column; gap: 5px; padding: 0 4px; flex: none; }
.diamond-search { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); font-size: var(--fs-xs); padding: 4px 8px; }
.diamond-search:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.diamond-search::placeholder { color: var(--text-muted); }
.diamond-search::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
/* The tag filter. A column: the row that names it, the pool of tags behind
   that, then the controls that are not tags. The agents panel puts a single chip
   straight in here, and a column of one looks the same as the row it used to
   be. */
.diamond-filter { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.tagf-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
/* The row the pool sits behind. Same place, same size and same ink as the
   "No tags yet" line it replaces the moment a tag exists, so the question "is
   there a filter here?" gets one quiet answer either way -- and the chips are
   one click behind it rather than permanently in front of the two lists.

   The whole row is the target. A rail is 250px of mostly empty space to the
   right of a short label, and a hit area that ends at the last letter is a
   control that gets missed. */
.tagf-toggle {
	display: flex;
	align-items: center;
	gap: 6px;
	align-self: stretch;
	background: none;
	border: none;
	padding: 1px 2px;
	text-align: left;
	font-size: var(--fs-2xs);
	line-height: 1.35;
	color: var(--text-muted);
	cursor: pointer;
}
.tagf-toggle:hover { color: var(--text-secondary); }
.tagf-toggle:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
/* Two borders on an empty box, turned 45°: a chevron that is ink rather than a
   character. Typed, it would be one missing glyph away from saying nothing, in
   an app that ships eight languages -- and it would ride in the row's text,
   where the tag count is read. */
.tagf-chev {
	flex: none;
	width: 5px;
	height: 5px;
	margin-bottom: 1px;		/* the turned square hangs low against the cap height */
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: rotate(-45deg);
	transition: transform 0.15s;
}
.tagf-toggle[aria-expanded="true"] .tagf-chev { transform: rotate(45deg); }
/* The pool: every tag in use, each chip cycling in place. One click behind the
   row above, which is what a filter nobody has to go looking for costs when the
   filing system is real -- the row is always there, the chips are not.

   Capped and scrolled past that. The rail's two lists live on what this leaves,
   so a vocabulary of thirty tags must not be allowed to take the rail off a
   reader who came for the Diamonds. A chip row is a 1.6em line box between two
   1px borders, and the cap is three and a HALF of them plus the flex gaps --
   stated in em against the pool's own font-size, so it follows the type scale
   rather than pinning one.

   The half row is the point of the number. A cap of exactly three would end on
   a row boundary, and thirty tags would look like nine: three tidy rows and no
   reason to suspect the rest. A row cut through the middle is the only thing
   in the box that says there is more of it. Under four rows the pool is shorter
   than the cap anyway, so nothing is cut when there is nothing to hide. */
.tagf-pool {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 4px;
	align-self: stretch;
	font-size: var(--fs-3xs);
	max-height: calc(5.6em + 19px);
	overflow-y: auto;
	overscroll-behavior: contain;	/* on the phone the drawer must not scroll with it */
}
/* Closed, with a filter on: the chips that are HOLDING it, standing on their
   own. A filter the reader cannot see is a rail hiding Diamonds for a reason it
   will not give. Same chips, same cycle, same order -- only the ones doing
   something -- so one tag filtering out of thirty costs one chip, not thirty.
   Capped like the pool: a filter can be built out of more tags than a rail can
   spare, and the state must be reachable rather than merely present. */
.tagf-active {
	align-self: stretch;
	font-size: var(--fs-3xs);
	max-height: calc(5.6em + 19px);
	overflow-y: auto;
	overscroll-behavior: contain;
}
/* The app's scrollbar is 14px, which is a fifth of this box's own height and a
   twentieth of the rail's width. */
.tagf-pool::-webkit-scrollbar,
.tagf-active::-webkit-scrollbar { width: 8px; }
/* Off, a pool chip is a plain chip, so only the cursor says it is a control. */
button.tag-pool { cursor: pointer; }
/* What is left below the pool once each chip holds its own state: how the
   wanted tags combine, and the one click that puts the whole filter down. */
.tagf-ctl { gap: 8px; }
/* No tags anywhere: one muted line where the chips would be, so an empty
   filing system does not look like a missing one. */
.rail-tag-hint { font-size: var(--fs-2xs); line-height: 1.35; color: var(--text-muted); }

/* Crystal view — the Diamond command surface (steer + fold). */
.crystal-view { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.crystal-body { flex: 1; overflow-y: auto; padding: 16px 20px; font-size: var(--fs-base); line-height: 1.6; color: var(--text-primary); word-break: break-word; }
.crystal-body .crystal-empty { color: var(--text-muted); font-style: italic; }
.crystal-controls { border-top: 1px solid var(--border); padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }

/* THE COMPOSER SITS BELOW THE CRYSTAL, not above it.
   `.chat-input-bar` comes BEFORE `.crystal-view` in the document, because it
   was written when only the chat thread ever used it and the crystal carried a
   steer box of its own. That box is gone and the composer is shared, so on the
   crystal face the source order puts the box at the top of the page -- above
   the document it is meant to sit under. Ordered explicitly rather than moved
   in the markup, because the chat face wants the source order it already has. */
.panel.ai.crystal-face .crystal-view   { order: 1; }
.panel.ai.crystal-face .chat-input-bar { order: 2; }

/* ── Full screen: the crystal, alone ─────────────────────────────────
   A capp is an application, and an application inside a panel inside an app
   reads on a phone as an app inside an app. `[data-cfull]` on the root element
   takes the app chrome away and leaves the page: the rail, the top bar, the
   dock, the other stage panels, the composer and the crystal's own bar.

   The mode is a USER's, and only a user's -- see `setCrystalFull` in
   js/daimond.js. Nothing here is a media query and nothing enters it by size:
   a phone gets the same panel it always had until somebody presses the button.

   WHAT IS NOT DONE HERE, deliberately: the panel is not lifted out of `#app`
   into a fixed, inset-0 box. `#app` already carries the safe-area insets in its
   own padding, so leaving the panel inside it is what keeps the way out clear
   of the notch; a fixed overlay would have to restate all four insets and would
   be wrong the first time one of them changed. */
:root[data-cfull] .topbar,
:root[data-cfull] #panel-rail,
:root[data-cfull] #handle-rail,
:root[data-cfull] .dock,
:root[data-cfull] .stage > .phandle,
:root[data-cfull] .stage > .panel:not(.ai),
:root[data-cfull] .panel.ai .chat-input-bar,
:root[data-cfull] .panel.ai .crystal-bar,
:root[data-cfull] .panel.ai .crystal-controls,
:root[data-cfull] .panel.ai #diamond-view,
:root[data-cfull] .panel.ai .chead-right > *:not(.cfull-btn) { display: none !important; }

/* The one panel left takes the stage, whatever inline width the layout engine
   last measured it at. */
:root[data-cfull] .stage > .panel.ai {
	flex: 1 1 auto !important;
	width: auto !important;
	display: flex !important;
}

/* The page runs to the panel's own edges. The gutter is chrome too.
   Only when there IS a page: `:has()` scopes this exactly as crystal.css scopes
   the flex column, so the built-in view -- which is prose in our own document --
   keeps the margin every other panel's text has. */
:root[data-cfull] .panel.ai .crystal-body:has(#crystal-frame-wrap) { padding: 0; }

/* The toggle. Two glyphs, one shown at a time, and a word beside the glyph
   ONLY while the mode is on: coming back is the thing a reader has to be able
   to do without working out what an icon means, and it is the one moment when
   there is room for the word because every other chip in the row has gone. */
.cfull-btn { display: inline-flex; align-items: center; gap: 5px; padding: 3px 6px; }
/* An explicit ring rather than the browser's own. In full screen this is the
   only control on the app, so where the keyboard is has to be legible against
   the accent ground it wears there. */
.cfull-btn:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 2px; }
.cfull-btn .ic { width: 16px; height: 16px; }
.cfull-btn .ic-cfull-out,
.cfull-txt { display: none; }
:root[data-cfull] .cfull-btn .ic-cfull-in { display: none; }
:root[data-cfull] .cfull-btn .ic-cfull-out { display: block; }
:root[data-cfull] .cfull-txt { display: inline; }
/* On in the sense the header's other latched chip is on, so the mode is
   readable from the control as well as from the screen behind it. */
:root[data-cfull] .cfull-btn { background: var(--accent); border-color: var(--accent); color: var(--on-fill, #fff); }

/* Artefact strip: what this Diamond produced or consulted. A count that hides at
   zero and opens a list, rather than a list that is always there taking room
   from the crystal above it and the steer box below. */
.arte-strip { display: block; width: 100%; text-align: left; background: none; border: none;
	color: var(--text-muted); font-size: var(--fs-xs); font-family: var(--font-mono);
	padding: 4px 2px; margin: 2px 0; cursor: pointer; }
.arte-strip:hover { color: var(--text-secondary); }
.arte-list { margin: 0 0 6px;
	border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-tertiary); }
.arte-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px 6px;
	min-height: var(--attach-row-h); box-sizing: border-box;
	border-bottom: 1px solid var(--border); }
.arte-row:last-child { border-bottom: none; }
.arte-kind { flex-shrink: 0; font-size: var(--fs-3xs-2xs); font-family: var(--font-mono);
	color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; min-width: 34px; }
.arte-open { flex: 1; min-width: 0; text-align: left; background: none; border: none;
	color: var(--text-primary); font-size: var(--fs-xs-sm); font-family: var(--font-mono);
	padding: 2px 0; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
button.arte-open:hover { color: var(--accent-text); }
/* A tile for something that cannot be opened. Not a button, because there is
   nothing to press -- and the reason is TEXT, not a hover title, because a
   tooltip is unavailable to a touch and an item that is silently inert on a
   phone is exactly the empty-folder failure this exists to end. Wanted twice
   over: ATTACH_CONTRACT.md §7 for an attachment from a workspace that is not
   open, and §9 for a Diamond in the trash. */
span.arte-open { cursor: default; }
.arte-open.away { color: var(--text-muted); font-style: italic; }
.arte-why { flex-basis: 100%; order: 9; padding: 0 0 3px 40px;
	font-size: var(--fs-3xs-2xs); color: var(--text-muted); line-height: 1.4; }
.arte-use, .arte-drop { flex-shrink: 0; background: none; border: none; cursor: pointer;
	color: var(--text-muted); font-size: var(--fs-sm); line-height: 1; padding: 2px 5px; }
.arte-use:hover { color: var(--accent); }
.arte-drop:hover { color: var(--danger, #e5534b); }

/* Note or Read, toggleable on the tile (ATTACH_CONTRACT.md §6). Note is the
   cheaper of the two by a wide margin, so it carries no colour of its own;
   Read spends the user's money and says so by picking up the accent, the same
   way a lit `.on` control does everywhere else in this app. */
.attach-state { flex-shrink: 0; background: none; border: 1px solid var(--border-2);
	border-radius: 999px; cursor: pointer; color: var(--text-secondary);
	font-size: var(--fs-3xs-2xs); line-height: 1.6; padding: 0 7px; }
.attach-state:hover { border-color: var(--accent); }
.attach-state.read { color: var(--accent-text); border-color: var(--accent); background: var(--accent-soft); }

/* A chat's queue for its next turn, above the composer -- ATTACH_CONTRACT.md
   §5: "showing exactly what the crystal footer shows". Reusing `.arte-list`
   is what makes that literally true rather than a promise to keep two things
   in step; this rule only adds the margin a footer above a composer needs
   that a list under a strip button did not. */
.chat-attach-strip { margin: 0 0 6px; }

/* ── A chat's footer is TWO GROUPS, and the difference is the point ──
   The workspace is a COLLECTIVE claim: take a folder out and the fence shrinks.
   So it is drawn as one bounded box with a rail down its edge -- one thing made
   of parts -- headed by the app's own words for it, `astat.workspace_browser`
   and `astat.workspace_native`, the same words the account strip uses.
   The attachments are individuals: a header and a plain list, no box, because
   dropping one changes nothing but itself.
   The strip's own frame comes off for this. A box inside a box reads as one
   thing with a mistake in it, and the two groups now carry their own edges. */
.chat-attach-strip { border: none; background: none; }
.attach-group + .attach-group { margin-top: 10px; }
/* Compounded with `.attach-head`, which is the class it also wears and which sets
   `justify-content: flex-end` for a band that is nothing but controls. Equal
   specificity would leave the winner to be decided by which rule came later in
   this file, and a group's NAME belongs at the left however the file is ordered. */
.attach-head.attach-group-head { justify-content: space-between; border-bottom: none;
	padding: 1px 3px 3px; }
.attach-group-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
	font-size: var(--fs-3xs-2xs); font-family: var(--font-mono); color: var(--text-muted);
	text-transform: uppercase; letter-spacing: .04em; }

/* ONE LIST since 2026-08-24, and therefore no accent rail down its edge. The
   rail said "everything in this box is in the fence", which was true while the
   box held only marked folders and stopped being true the moment the two groups
   became one: it would have bracketed attachments that grant nothing. The claim
   is now made once, on the row that makes it -- the `Workspace` pill below --
   rather than twice, once wrongly.
   The cap is `--attach-cap`, the FOOTER's own, because this is the whole footer
   and not the upper of two groups. */
.ws-body { border: 1px solid var(--border-strong);
	border-radius: var(--radius); background: var(--bg-tertiary);
	max-height: calc(var(--attach-cap) * var(--attach-row-h) + var(--attach-peek));
	overflow-y: auto; scrollbar-gutter: stable; scrollbar-width: thin; }
.ws-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ws-body::-webkit-scrollbar { width: 10px; }
.ws-body::-webkit-scrollbar-track { background: var(--bg-secondary); border-radius: 5px; }
.ws-body::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 5px;
	border: 2px solid var(--bg-secondary); }
/* The sentence for the empty fence. It wraps, it is the tallest thing this
   group is ever drawn at, and it is deliberately not a hint about a button:
   what it says is what the chat can reach. */
.ws-empty { padding: 7px 9px; line-height: 1.45; }

/* One list since 2026-08-24, so there is no second group to make room for and
   no `.two` arithmetic to do. What tells a marked folder from an attached file
   is the row itself: the `Workspace` pill below, pressed or not. */

/* The workspace mark, on a folder's tile in either group: SQUARE, where
   Note/Read is round. Two independent things sit side by side here, and one of
   them decides what the agent may change -- telling them apart must not depend
   on reading two short words, or on telling two accent tints apart. */
.attach-ws { flex-shrink: 0; background: none; border: 1px solid var(--border-2);
	/* 3px, not `--radius-sm`: at this height every radius the app has still reads
	   as a pill beside the round one, and the two controls must be told apart
	   without reading either word. */
	border-radius: 3px; cursor: pointer; color: var(--text-secondary);
	font-size: var(--fs-3xs-2xs); line-height: 1.6; padding: 0 7px; }
.attach-ws:hover { border-color: var(--accent); }
.attach-ws.on { color: var(--accent-text); border-color: var(--accent); background: var(--accent-soft); }

/* The chrome both footers wear: the view toggle and `+`, above the tiles and
   outside the scroll, because chrome that scrolls away is chrome nobody finds
   a second time. */
.attach-head { display: flex; align-items: center; justify-content: flex-end; gap: 4px;
	padding: 3px 5px; border-bottom: 1px solid var(--border); }
/* `--border-strong`, the control boundary, NOT `--border-2`, which divides one
   region from another. A control that borrows the divider token disappears at
   its own edge in the paler palettes, and this is the rule verify_theme keeps:
   the failure it catches is a new control written by copying an old one. */
.attach-view-btn { background: none; border: 1px solid var(--border-strong); border-radius: 999px;
	cursor: pointer; color: var(--text-secondary); font-size: var(--fs-3xs-2xs);
	line-height: 1.6; padding: 0 9px; }
.attach-view-btn:hover { border-color: var(--accent); color: var(--accent-text); }
/* It carries a WORD now, so it is shaped like the pill beside it rather than
   like an icon: a bare `+` said nothing, and the two of them a few pixels apart
   -- one granting a fence, one not -- read as the same button. */
.attach-add { background: none; border: 1px solid var(--border-strong); border-radius: 999px;
	cursor: pointer; color: var(--text-secondary); font-size: var(--fs-3xs-2xs);
	line-height: 1.6; padding: 0 9px; min-height: 22px; }
.attach-add:hover { border-color: var(--accent); color: var(--accent-text); }
/* And a `+` that widens what a daimon may write wears the accent every other
   granting control in this app wears, so the two are told apart without reading
   either one. */
.attach-add.grants { color: var(--accent-text); border-color: var(--accent);
	background: var(--accent-soft); }
.attach-add.grants:hover { background: var(--accent); color: var(--on-fill, #fff); }

/* The legend at the head of the picker, where a tick on a folder and a tick on
   a file mean two different things and no row has room to say which. */
.attach-pick-note { padding: 6px 2px 8px; line-height: 1.45;
	font-size: var(--fs-3xs-2xs); color: var(--accent-text); }

/* The tiles themselves, and the only thing in either footer that scrolls. The
   cap is `--attach-cap` tiles, which is six and is measured -- variables.css
   says how it was arrived at. `scrollbar-gutter: stable` keeps the width from
   twitching as the last tile is taken off, and the track is painted rather
   than left as an overlay that fades: the contract asks for a footer that
   LOOKS like it scrolls. */
.attach-body { max-height: calc(var(--attach-cap) * var(--attach-row-h) + var(--attach-peek));
	overflow-y: auto; scrollbar-gutter: stable; scrollbar-width: thin; }
.attach-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.attach-body::-webkit-scrollbar { width: 10px; }
.attach-body::-webkit-scrollbar-track { background: var(--bg-secondary); border-radius: 5px; }
.attach-body::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 5px;
	border: 2px solid var(--bg-secondary); }

/* The other view: rows of sizeable icons (§5), which the note is explicit are
   NOT button-sized. An 88x84 cell against the 26px buttons beside it -- big
   enough to hit without aiming, and to read the name off at a glance. */
.attach-body.icons { display: flex; flex-wrap: wrap; align-content: flex-start;
	gap: 8px; padding: 8px; }
.attach-icon { position: relative; width: var(--attach-icon-w); height: var(--attach-icon-h);
	box-sizing: border-box; display: flex; flex-direction: column; align-items: center;
	gap: 2px; padding: 6px 4px 4px; border: 1px solid var(--border); border-radius: var(--radius);
	background: var(--bg-secondary); }
.attach-icon .arte-kind { min-width: 0; font-size: 26px; line-height: 1.05; }
.attach-icon .arte-open { flex: none; width: 100%; text-align: center; white-space: normal;
	overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
	font-size: var(--fs-3xs-2xs); line-height: 1.25; word-break: break-word; }
.attach-icon .attach-state { margin-top: auto; }
/* The reason a shut tile gives is a sentence, and a sentence does not fit an
   88px cell. On an icon it is the hover title and the cell says `?`; the stack
   view is where it is written out, and the toggle is one press away. */
.attach-icon .arte-why { position: absolute; top: 2px; left: 4px; padding: 0;
	font-size: var(--fs-3xs-2xs); color: var(--text-muted); overflow: hidden;
	width: 12px; height: 12px; white-space: nowrap; text-indent: -999px; }
.attach-icon .arte-why::after { content: '?'; position: absolute; left: 0; top: 0; text-indent: 0; }
.attach-icon.shut { border-style: dashed; }
/* The refer arrow and the × sit in the corner of an icon rather than in a row
   of their own: a cell this size has no row to spare. */
.attach-icon .arte-use, .attach-icon .arte-drop { position: absolute; top: 0; padding: 1px 4px;
	font-size: var(--fs-xs); opacity: 0; }
.attach-icon .arte-use { right: 16px; }
.attach-icon .arte-drop { right: 0; }
.attach-icon:hover .arte-use, .attach-icon:hover .arte-drop,
.attach-icon:focus-within .arte-use, .attach-icon:focus-within .arte-drop { opacity: 1; }

/* The `+` picker: the workspace's own tree, in the app's own dialog, with a
   tick per row. Nothing here is a new modal -- see `dialog`'s `pick` kind. */
.attach-pick { max-height: 46vh; overflow-y: auto; margin: 8px 0 0;
	border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-tertiary); }
.attach-pick-path { border-bottom: none; padding: 4px 2px; }
.attach-pick-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px;
	border-bottom: 1px solid var(--border); }
.attach-pick-row:last-child { border-bottom: none; }
.attach-pick-tick { flex: none; width: 16px; height: 16px; cursor: pointer; accent-color: var(--accent); }
.attach-pick-tick:disabled { cursor: default; }
.attach-pick-name { flex: 1; min-width: 0; text-align: left; background: none; border: none;
	padding: 0; color: var(--text-primary); font-size: var(--fs-xs-sm);
	font-family: var(--font-mono); cursor: pointer;
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-pick-name.dir:hover { color: var(--accent-text); }

/* Links: how this Diamond stands to the other Diamonds. The artefact strip
   above holds the links pointing at a file, a page or a chat; these are the
   ones pointing at another Diamond, which is a different question. Unlike the
   strip this header never hides, because it is also the only way in to making
   a link, and a control that appears once there is already one of the thing it
   makes is a control nobody finds. */
.link-sec { margin: 2px 0 6px; }
.link-strip { display: block; width: 100%; text-align: left; background: none; border: none;
	color: var(--text-muted); font-size: var(--fs-xs); font-family: var(--font-mono);
	padding: 4px 2px; cursor: pointer; }
.link-strip:hover { color: var(--text-secondary); }
.link-body { border: 1px solid var(--border); border-radius: var(--radius);
	background: var(--bg-tertiary); padding: 6px; display: flex; flex-direction: column; gap: 6px; }
.link-list { max-height: 30vh; overflow-y: auto; }
.link-row { border-bottom: 1px solid var(--border); padding: 3px 2px; }
.link-row:last-child { border-bottom: none; }
/* The direction is what a row is FOR, so it is said twice over: by the order of
   the two ends in the phrase, and by which side the coloured edge sits on. */
.link-row-out { border-left: 2px solid var(--accent); padding-left: 6px; }
.link-row-in { border-right: 2px solid var(--border-2); padding-right: 6px; }
.link-line { display: flex; align-items: center; gap: 6px; }
.link-phrase { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.link-self { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted);
	text-transform: uppercase; letter-spacing: .04em; }
.link-arrow { color: var(--text-muted); font-size: var(--fs-xs); }
.link-rel { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary);
	background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 20px;
	padding: 1px 8px; white-space: nowrap; }
.link-other { background: none; border: none; padding: 2px 0; cursor: pointer;
	color: var(--text-primary); font-size: var(--fs-xs-sm); text-align: left;
	max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.link-other:hover { color: var(--accent-text); }
.link-acts { flex: none; display: flex; gap: 2px; }
.link-note-btn, .link-drop { background: none; border: none; cursor: pointer;
	color: var(--text-muted); font-size: var(--fs-sm); line-height: 1; padding: 2px 5px;
	min-width: 26px; min-height: 26px; }
.link-note-btn:hover, .link-note-btn.on { color: var(--accent); }
.link-drop:hover { color: var(--danger, #e5534b); }
.link-note { font-size: var(--fs-xs); color: var(--text-secondary); line-height: 1.5;
	padding: 2px 2px 4px 6px; }
.link-none { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; padding: 4px 2px; }
.link-add { align-self: flex-start; font-size: var(--fs-xs); padding: 6px 11px; min-height: 34px;
	background: var(--bg-secondary); color: var(--text-secondary);
	border: 1px solid var(--border-strong); border-radius: var(--radius-sm); cursor: pointer; }
.link-add:hover { background: var(--bg-hover); color: var(--text-primary); }

/* The add form. Every value it holds lives in JS rather than in these fields,
   so a repaint under a half-filled form restores it instead of emptying it. */
.link-form { display: flex; flex-direction: column; gap: 8px;
	border-top: 1px solid var(--border); padding-top: 8px; }
.link-field { display: flex; flex-direction: column; gap: 4px; }
.link-label { font-size: var(--fs-2xs); font-weight: 600; letter-spacing: .04em;
	text-transform: uppercase; color: var(--text-muted); }
.link-pick, .link-rel-input, .link-note-input { background: var(--bg-secondary);
	border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary);
	font-size: var(--fs-sm); padding: 6px 8px; min-height: 34px; width: 100%; }
.link-pick:focus, .link-rel-input:focus, .link-note-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.link-pick::placeholder, .link-rel-input::placeholder, .link-note-input::placeholder { color: var(--text-muted); }
.link-picks { display: flex; flex-wrap: wrap; gap: 4px; }
.link-pick-hit { font-size: var(--fs-xs); padding: 6px 10px; min-height: 34px; cursor: pointer;
	background: var(--bg-secondary); color: var(--text-primary);
	border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
	max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.link-pick-hit:hover { border-color: var(--accent); background: var(--bg-hover); }
.link-chosen { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.link-chosen-name { font-size: var(--fs-sm); color: var(--text-primary); }
.link-change { font-size: var(--fs-xs); padding: 6px 10px; min-height: 34px; cursor: pointer;
	background: none; color: var(--text-secondary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); }
.link-change:hover { color: var(--text-primary); }
.link-rel-sug { display: flex; flex-wrap: wrap; gap: 4px; }
.link-sug { font-family: var(--font-mono); font-size: var(--fs-2xs); padding: 5px 9px;
	min-height: 28px; cursor: pointer; background: none; color: var(--text-muted);
	border: 1px dashed var(--border); border-radius: 20px; }
.link-sug:hover { color: var(--text-primary); border-style: solid; border-color: var(--accent); }
.link-says { font-size: var(--fs-2xs); color: var(--text-muted); line-height: 1.5; }
.link-err { font-size: var(--fs-xs); color: var(--danger, #e5534b); }
.link-err:empty { display: none; }
.link-form-acts { display: flex; gap: 6px; }
.link-save, .link-cancel { font-size: var(--fs-xs); padding: 7px 13px; min-height: 34px;
	border-radius: var(--radius-sm); cursor: pointer; }
.link-save { background: var(--accent); color: var(--on-fill, #fff); border: 1px solid var(--accent); }
.link-save:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.link-save:disabled { opacity: .5; cursor: default; }
.link-cancel { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-strong); }
.link-cancel:hover { background: var(--bg-hover); color: var(--text-primary); }

/* At a phone's width every target is a thumb's target, and the phrase wraps
   rather than pushing a row's controls off the edge. */
@media (max-width: 420px) {
	.link-note-btn, .link-drop { min-width: 34px; min-height: 34px; font-size: var(--fs-base); }
	.link-sug { min-height: 34px; padding: 7px 11px; }
	.link-body { padding: 6px 5px; }
}

/* Fold-a-delta control: enter a delta, propose a fold (writes nothing). */
.crystal-status { font-size: var(--fs-xs); color: var(--text-muted); font-family: var(--font-mono); min-height: 14px; }
/* The waiter on the crystal face. The dots are `.chat-spinner-dot`, the same
   class the thread's spinner uses, so the two faces cannot drift and the
   reduced-motion rule below already covers these. Only the box around them is
   new: inline-flex so the dots sit ON the status line beside its words rather
   than above them, and a right margin because `gap` would set the space before
   the sentence to the (much tighter) space between the dots. */
.crystal-dots { display: inline-flex; align-items: center; gap: 3px; margin-right: 6px; vertical-align: middle; }
.crystal-dots .chat-spinner-dot { width: 5px; height: 5px; }
/* --panel-2 and --text were never declared, so this box fell back to a white
   wash and a white hover: invisible surface on the light themes, invisible × on
   both of them. Every colour here is a real token now. */
.crystal-reply { position: relative; margin: 6px 0; padding: 8px 26px 8px 10px; border-radius: 8px;
	background: var(--bg-tertiary); border: 1px solid var(--border);
	font-size: var(--fs-sm); line-height: 1.45; max-height: 40vh; overflow-y: auto; }
.crystal-reply-body > :first-child { margin-top: 0; }
.crystal-reply-body > :last-child { margin-bottom: 0; }
.crystal-reply-x { position: absolute; top: 4px; right: 6px; border: none; background: none;
	color: var(--text-muted); cursor: pointer; font-size: var(--fs-xl); line-height: 1; padding: 2px 4px; }
.crystal-reply-x:hover { color: var(--text-primary); }

/* The reducer is a paid round trip and the slowest thing in the app, so the
   crystal shows the same three dots the thread does while it runs. */
.crystal-spinner { padding: 8px 2px; }

/* A fold proposed on a Diamond that is not the one on screen. It sits with the
   Diamond's other plain facts in the rail, and is the only thing that says there
   is a proposal waiting to be accepted or rejected. */
.diamond-pending { font-size: var(--fs-2xs); color: var(--accent-text); font-weight: 600; }

/* Fold diff — added / removed lines, all text escaped. */
.diff-head { font-size: var(--fs-xs); color: var(--text-secondary); margin-bottom: 10px; }
/* A key the proposal would drop. Warning colour, not error: this is a thing the user may
   well want, and the point is that they see it rather than that they are stopped. */
.diff-lost {
	font-size: var(--fs-xs);
	color: var(--warn);
	border-left: 3px solid var(--warn);
	padding: 6px 10px;
	margin-bottom: 10px;
	background: var(--warn-bg);
	border-radius: var(--radius-sm);
}
.diff-lines { font-family: var(--font-mono); font-size: var(--fs-xs-sm); line-height: 1.55; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.diff-line { display: block; white-space: pre-wrap; word-break: break-word; padding: 1px 8px; }
.diff-line .sign { display: inline-block; width: 1.3em; color: var(--text-muted); user-select: none; }
.diff-line.add { background: rgba(102, 185, 140, 0.16); }
.diff-line.del { background: rgba(224, 122, 103, 0.16); }
.diff-line.add .sign { color: var(--ok); }
.diff-line.del .sign { color: var(--danger); }
.diff-actions { display: flex; gap: 8px; }
.diff-accept { background: var(--ok); color: var(--on-fill, #fff); border: none; border-radius: var(--radius); padding: 9px 16px; font-size: var(--fs-base); }
.diff-accept:hover { filter: brightness(1.08); }
.diff-reject { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 9px 16px; font-size: var(--fs-base); }
.diff-reject:hover { border-color: var(--danger); color: var(--danger); }

/* Empty state */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--text-secondary); gap: 10px; padding: 24px; }
.empty-state h2 { margin: 0; color: var(--text-primary); font-size: var(--fs-3xl); }
.empty-state p { margin: 0; max-width: 360px; }
/* The empty chat opens straight on its one action, with the welcome copy removed. */
.empty-state.bare { gap: 0; }
.empty-new-session { margin-top: 12px; background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius); padding: 10px 18px; font-size: var(--fs-base); }
.empty-state.bare .empty-new-session { margin-top: 0; }
.empty-new-session:hover { background: var(--accent-hover); }

/* The turn indicator: three dots and a line saying what the turn is doing.
   It is up for the whole turn, so it sits at the foot of the thread and the
   caption is what stops it being four minutes of unchanging nothing. */
.chat-spinner { display: flex; align-items: center; gap: 4px; padding: 8px 14px; }
/* The caption. Muted and small: it is a status line beside the answer, not part
   of it, and it must not read as something the model said. The left margin is
   the gap between the dots and the words -- `gap` alone would set it to the
   spacing BETWEEN the dots, which is far too tight for a sentence. */
.chat-spinner-say { margin-left: 6px; font-size: var(--fs-xs); color: var(--text-secondary); }
.chat-spinner-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--text-secondary); animation: spinner-bounce 1.4s infinite ease-in-out both; }
.chat-spinner-dot:nth-child(1) { animation-delay: -0.32s; }
.chat-spinner-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes spinner-bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
/* A reader who has asked for less motion still needs to know the model is
   working, so the dots stay -- they stop moving. `animation: none` alone would
   not do: the keyframe rests them at scale(0), so switching it off mid-cycle
   leaves three invisible dots and a spinner that says nothing. */
@media (prefers-reduced-motion: reduce) {
	.chat-spinner-dot { animation: none; transform: scale(1); opacity: 0.5; }
	.chat-spinner-dot:nth-child(2) { opacity: 0.75; }
	.chat-spinner-dot:nth-child(3) { opacity: 1; }
}

/* Inline tool-call blocks */
.tool-block { margin: 8px 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-tertiary); overflow: hidden; font-size: var(--fs-sm); }
.tool-head { padding: 7px 12px; cursor: pointer; font-weight: 600; color: var(--accent-text); user-select: none; display: flex; align-items: center; gap: 6px; }
.tool-head::before { content: '▾'; font-size: var(--fs-3xs); color: var(--text-muted); transition: transform 0.15s; }
.tool-block.collapsed .tool-head::before { transform: rotate(-90deg); }
.tool-head:hover { background: var(--bg-hover); }
.tool-block.running .tool-head::after { content: '…'; color: var(--text-secondary); }
.tool-block.failed { border-color: var(--danger); }
.tool-block.failed .tool-result { color: var(--danger); }
.tool-args, .tool-result { margin: 0; padding: 8px 12px; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: var(--fs-xs); border-top: 1px solid var(--border); max-height: 260px; overflow: auto; }
.tool-args { color: var(--text-secondary); }
.tool-result { color: var(--text-primary); }
/* Why a command in this block could not reach anything. NOT a failure -- the fence
   did what the user asked -- so the warning colour the refusals use and never the
   danger one, and prose rather than the monospace the tool's own output is in. */
.tool-nonet { padding: 8px 12px; border-top: 1px solid var(--border); color: var(--warn); font-size: var(--fs-xs); }
/* Collapsed by default: hide the (often long) args JSON, keep the result. */
.tool-block.collapsed .tool-args { display: none !important; }
.chat-output.hide-tools .tool-block { display: none; }
/* The model thinking out loud on its way to a tool call, and IT DOES NOT HIDE WITH THE
   STEPS. It did until 2026-08-28, on the reasoning that a reader who turned the working
   off meant this too, and what that actually did was take the model's own sentences off
   the screen: a turn of twenty calls kept nothing but its last paragraph. The switch is
   called Steps and it hides the steps. See `demoteToWorking` in www/js/daimond.js. */
.chat-msg-working { margin: 4px 0 4px 2px; }
.chat-working-line {
	color: var(--text-muted);
	font-size: var(--fs-sm);
	font-style: italic;
	opacity: 0.72;
}
/* How much of the model's working went behind the control. Quieter than the summary
   it sits after, because it is a measurement and not a sentence -- but it is THERE,
   which is the point: a summary alone reads as text that went away rather than as a
   control that opens on it. See `demoteToWorking` in www/js/daimond.js. */
.chat-working-more {
	margin-left: 6px;
	color: var(--text-muted);
	font-size: var(--fs-xs);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* Context-window meter (session boxes + agent tiles) */
.ctx-meter { display: flex; align-items: center; gap: 6px; margin-top: 5px; }
.ctx-bar { flex: 1; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
.ctx-fill { height: 100%; background: var(--accent); transition: width 0.3s ease; }
.ctx-fill.high { background: var(--danger); }
.ctx-meter-label { font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-secondary); white-space: nowrap; }

/* ── Agents panel ────────────────────────────────────────────── */
.agents-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
/* 10px, which is the inset every dock panel's first body line now takes. The
   four disagreed — Agents 6, Email 10, Spending 14, Workspace 11 — and four
   panels side by side in one dock with four different left edges is the kind of
   thing you see without being able to name. 10 reads well against the 4px
   header without moving the header. Email already had it; mail.css, files.css
   and spend.css carry the same number. */
.agents-empty { font-size: var(--fs-xs); color: var(--text-muted); padding: 2px 10px; }
.agent-find { padding-top: 2px; padding-bottom: 2px; }
/* Chips on an agent tile: parent Diamond, its inherited tags, the model. */
.acard .achips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin: 4px 0 2px; }
.acard .diamond-chip { font-weight: 500; }
.acard .achip-model { font-family: var(--font-mono); font-size: var(--fs-3xs); line-height: 1.6; padding: 0 6px; border-radius: 20px; max-width: 128px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); background: var(--bg-tertiary); border: 1px solid var(--border); }
/* A worker routed by modality. Marked rather than recoloured: which model a run
   is on is the fact, and how it came to be on it is a note about that fact. */
.acard .achip-model.achip-vision { border-style: dashed; }
/* A worker that was MOVED names both models in the chip itself -- the one it left
   and the one it went to. Wider than an ordinary model chip because it carries two
   names, and it must not ellipse away the half that says anything moved. */
.acard .achip-model.achip-rerouted { max-width: 232px; border-style: dashed; border-color: var(--accent); color: var(--text-secondary); }
/* What the app did to a picture, in the app's own voice, under the chips. Not a
   title: a tooltip is not disclosure, and a tooltip was all this said on the night
   it sent a whole run to a model that could not see. */
.acard .anote { font-size: var(--fs-2xs); line-height: 1.35; margin: 3px 0 0; color: var(--text-muted); }
.acard .anote.avision { color: var(--text-secondary); }

.acard { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 10px; padding: 8px 9px; flex: none; }
.acard.running { border-color: var(--border-2); }
.acard.done { border-color: var(--ok); }
.acard.error { border-color: var(--danger); }
.acard .ah { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 3px; }
.acard .an { font-size: var(--fs-xs); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pill { font-family: var(--font-mono); font-size: var(--fs-3xs); padding: 1px 8px; border-radius: 20px; white-space: nowrap; flex: none; }
.pill.run { color: var(--accent-text); background: var(--accent-soft); }
.pill.ok { color: var(--ok); background: var(--ok-bg); }
.pill.err { color: var(--danger); background: var(--accent-soft); }
.acard .arow { display: flex; justify-content: space-between; gap: 6px; font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); }
.atools { margin-top: 5px; display: flex; flex-direction: column; gap: 2px; }
.atool { display: flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary); cursor: default; padding: 1px 2px; border-radius: 4px; overflow: hidden; white-space: nowrap; }
/* Only a row that can actually take you somewhere offers the affordance. */
.atool.linked { cursor: pointer; }
.atool.linked:hover { background: var(--bg-hover); color: var(--text-primary); }
.atool.failed { color: var(--danger); }
.atool .cross { color: var(--danger); flex: none; }
.atool .tick { color: var(--ok); flex: none; }
.atool .live { color: var(--accent); flex: none; animation: atool-pulse 1.2s infinite ease-in-out; }
@keyframes atool-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
/* The running tool keeps its accent colour instead of pulsing: the state was
   carried by the movement, so something has to carry it once the movement is
   gone. */
@media (prefers-reduced-motion: reduce) {
	.atool .live { animation: none; opacity: 1; font-weight: 600; }
}

/* ── Mobile nav (hidden on desktop; see responsive.css) ──────── */
.mnav { display: none; }

/* ── The closer, and the row it sits in ──────────────────────────
   Built by www/js/closer.js, worn by every dialog, popover, drawer and sheet.
   One set of rules, because five spellings of one control is how four of them
   ended up under the thumb's floor.

   28px here and 44px where the pointer is coarse (see responsive.css). The
   drawn cross keeps `.ic`'s 16px in both: the TARGET is a thumb's and the INK
   is an eye's, and growing the ink with the box would put a great black X in
   the corner of every card on a phone.

   In flow, never absolute. `.modal-card` and `.pop` are both `overflow-y:
   auto`, so an absolutely positioned closer would resolve against the initial
   containing block -- 4px from the top of the VIEWPORT rather than of the card
   -- and would stay there while the content scrolled underneath it. */
.ui-close {
	flex: none;
	display: flex; align-items: center; justify-content: center;
	width: 28px; height: 28px; padding: 0;
	border: none; border-radius: var(--radius-sm); background: none;
	color: var(--text-muted);
	cursor: pointer;
}
.ui-close:hover { color: var(--text-primary); background: var(--bg-hover); }
.ui-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The title row: what the surface is, and the way out beside it. `flex-start`
   rather than centre, because a long name wraps and would otherwise drag the
   cross down the side of it. */
.ui-head {
	display: flex; align-items: flex-start; justify-content: space-between;
	gap: 12px; margin-bottom: 10px;
}
/* The row carries the space the heading used to carry itself; left on, the
   heading's margin would open a gap under the cross as well as under the
   words. */
.ui-head > :first-child { flex: 1 1 auto; min-width: 0; }
.ui-head h2 { margin-bottom: 0; overflow-wrap: anywhere; }
/* A dialog's own heading keeps its type; a popover's title is a label, so it
   is set like one. Not `.pop-head`, which marks a SECTION inside the popover:
   a second one of those at the top would read as an empty first section. */
.ui-head-title {
	font-size: var(--fs-sm); font-weight: 600;
	color: var(--text-secondary);
	overflow-wrap: anywhere;
}
.pop > .ui-head { margin-bottom: 4px; padding: 2px 0 2px 6px; }

/* Modal */
.modal { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; width: 400px; max-width: 90vw; max-height: 80vh; overflow-y: auto; }
.modal-card h2 { font-size: var(--fs-2xl); margin-bottom: 16px; }
/* The 8px that puts every Admin view on one left edge. `#admin-scroll` gives
   8px to all of them; Home's rows add another 8 of their own (`.admin-item`,
   `.admin-sec`) and the form views added none, so switching from Home to Models
   or Credits shifted every word 8px left. Measured off the ink: Home 16, Models
   8, Credits 8. */
.settings-section { margin-bottom: 20px; padding: 0 8px; }
.settings-section h3 { font-size: var(--fs-lg); margin-bottom: 12px; color: var(--text-secondary); }
.settings-section form { display: flex; flex-direction: column; gap: 10px; }
.settings-section input { background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 9px 12px; font-size: var(--fs-base); }
.settings-section input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.settings-select { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 9px 12px; font-size: var(--fs-base); }
/* ── Why a pulldown says `:focus-visible` and a text field does not ──
   The accent ring means "the keyboard is here". A mouse click that paints it
   says something untrue, which is why the forty-odd button rules in this app
   are `:focus-visible` -- and this rule, alone among the pulldowns, was not.

   The two selectors are NOT interchangeable, and which controls they differ on
   was measured rather than assumed (a real `page.mouse.click`, the pointer then
   parked off the control so `:hover` could not be mistaken for a ring):

     control            Chromium :focus-visible   Firefox :focus-visible
     <select>           true                      FALSE
     <input type=text>  true                      true
     <button>           false                     false
     [tabindex] div     false                     false

   So on a pulldown the change is real -- Firefox stops ringing a clicked
   select, which is what Firefox does for every other select on the web -- while
   on a TEXT FIELD it paints identically in both engines, because a control that
   takes typing always matches `:focus-visible` by design. The app's text-field
   `:focus` rules are therefore left exactly as they are: changing them moves no
   pixel in any engine, and the caret is what says typing will land there. */
.settings-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.settings-select option:disabled { color: var(--text-muted); }
.settings-section button { background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 9px 16px; font-size: var(--fs-base); }
.settings-section button:hover { background: var(--accent-hover); }
.modal-close { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 8px 16px; font-size: var(--fs-base); margin-top: 8px; width: 100%; }
.modal-close:hover { background: var(--bg-hover); color: var(--text-primary); }

/* BYOK settings helpers (browser-only Daimond). */
.cfg-check { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); color: var(--text-secondary); }
.cfg-check input { width: auto; }
.cfg-hint { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 10px; line-height: 1.5; }
.cfg-credits-hint { color: var(--text-secondary); }
.cfg-lead { font-size: var(--fs-sm); color: var(--text-secondary); margin: -4px 0 4px; line-height: 1.5; }
.byok-form { display: flex; flex-direction: column; gap: 10px; }
.byok-form input[type="text"] { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 9px 12px; font-size: var(--fs-base); }
.byok-form input[type="text"]:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.byok-form input::placeholder { color: var(--text-muted); }
#byok-save { width: 100%; background: var(--accent); color: var(--on-fill, #fff); border: 1px solid var(--accent); border-radius: var(--radius-sm); padding: 9px 16px; font-size: var(--fs-base); margin-top: 2px; }
#byok-save:hover { filter: brightness(1.08); }
/* The default suits a plain block host (the admin card's built forms). The
   BYOK form spaces rows with a flex gap instead, so it compensates below --
   a negative default here put every OTHER form's label flush on its box. */
.cfg-fieldlabel { display: block; font-size: var(--fs-xs); color: var(--text-secondary); margin: 12px 0 6px; }
.cfg-fieldlabel:first-child { margin-top: 0; }
/* A CHILD of the form, not a descendant. The -4px only makes sense where the
   form's own 10px gap is above the label; #cfg-url-row is a plain div holding
   the Base URL pair, no gap reaches inside it, and the compensation there put
   the label 4px down onto its own box. As the row's first child the label
   already takes margin-top:0 from the rule above, so it needs nothing else. */
.byok-form > .cfg-fieldlabel { margin: 0 0 -4px; }
/* Same trap on the other side of the field, and the one the label's fix missed.
   Three pulldowns are inserted into the Models section as plain blocks (steps
   per turn, longest reply, crystal size) and the Push form is a block too, so
   the note rode up into the box it explains. 6px in both hosts: the note binds
   to the field above it, and the 12px before the next label keeps it there. */
.cfg-fieldnote { font-size: var(--fs-xs); color: var(--text-muted); margin: 6px 0 0; line-height: 1.5; }
.byok-form .cfg-fieldnote { margin: -4px 0 0; }
/* A heading over a PAIR of rows that are one setting to a person -- the crystal's
   data limit and its page limit. Presented apart they read as two unrelated
   numbers. Heavier than a field label and lighter than the section title, with
   a rule under it so the two rows beneath read as belonging to it. */
.cfg-fieldhead { font-size: var(--fs-sm); font-weight: 600; color: var(--text-primary); margin: 18px 0 0; padding-bottom: 5px; border-bottom: 1px solid var(--border); }
.cfg-fieldhead + .cfg-fieldlabel { margin-top: 8px; }
.cfg-advanced summary { font-size: var(--fs-sm); color: var(--text-secondary); cursor: pointer; user-select: none; padding: 2px 0; }
.cfg-advanced[open] { display: flex; flex-direction: column; gap: 10px; }

/* ── Search: which service the agent searches with ──────────────
   A row is a label, its field and the notes that explain it, kept together so
   the notes cannot drift onto the field below on a narrow rail. Same weights
   as the model picker directly above it, because it is the same kind of
   decision and a second visual language for it would say otherwise. */
.search-engine-row { display: flex; flex-direction: column; margin-top: 12px; }
.search-engine-row:first-of-type { margin-top: 0; }
.search-engine-row input[type="text"] {
	width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); color: var(--text-primary);
	padding: 9px 12px; font-size: var(--fs-base);
}
.search-engine-row input[type="text"]:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.search-engine-row input::placeholder { color: var(--text-muted); }
.search-engine-note { font-size: var(--fs-xs); color: var(--text-muted); margin: 6px 0 0; line-height: 1.5; }
.search-engine-note a { color: var(--accent-text); }
/* AMBER, not red. "You have not added a key yet" is a state of the setting and
   a thing to do about it; drawing it as an error would report a fault at
   somebody who has done nothing wrong -- the same rule the credits row follows
   when the balance runs out. */
.search-engine-note[data-warn] { color: var(--warn); }

/* `root/web`'s control, in the Web panel header. Ruled off from the page
   buttons beside it because it is a different KIND of control: back, reload
   and pop act on the page, and this decides whether the panel may spend at
   all. Without the rule it reads as a fourth page button with a lamp on it. */
.web .chead-right .pptw { padding-right: 8px; border-right: 1px solid var(--border); }

/* ── Chat tiles: pending (pick model → Start) + active (meter + Fold) ─── */
/* A chat's label is DERIVED and no longer an editable field: it is a button
   whose only act is opening the chat. It keeps the field's metrics so the rail
   does not reflow, and loses the field's hover border, which used to be an
   invitation to type. `text-align: left` because a button centres by default
   and every other line on the tile is ranged left. */
.tile-label { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 6px; background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); color: var(--text-primary); font-size: var(--fs-sm); font-weight: 500; padding: 2px 5px; text-align: left; overflow: hidden; }
.tile-label:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.session-box.active .tile-label { color: var(--accent-text); }
/* The identity itself: a name if one was set, otherwise the relative time. */
.tile-when { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The clock beside a name that WAS set. Quieter than the name and never
   shrinking below its own content, so a long name is what gets clipped -- the
   time is the one thing on this row that is the same width for every tile. */
.tile-when-sub { flex: none; font-size: var(--fs-2xs); font-weight: 400; color: var(--text-muted); }
.session-box.active .tile-when-sub { color: inherit; opacity: 0.75; }
/* The first thing said in the chat, dimmed, on its own line. ONE line, clipped
   with an ellipsis: two would double the height of every tile in the rail, and
   the whole line is on the `title` for anyone who wants it. */
.tile-preview { margin-top: 3px; font-size: var(--fs-2xs); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-box.active .tile-preview { color: inherit; opacity: 0.7; }
/* NOT a preview: a chat whose transcript is on another device and has not
   arrived. It sits in the preview's place and inherited the preview's paint, so
   the one tile a reader most needs told apart from an empty chat was drawn
   exactly like the first line of a full one. Italic and one notch quieter, and
   the app's own "waiting" colour rather than a fourth grey — it is a state, not
   a quotation, and nothing about it is the user's words. */
.tile-waiting { font-style: italic; opacity: 0.85; }
.session-box.active .tile-waiting { color: inherit; opacity: 0.6; }
/* The day heading above a run of tiles. Furniture, not a control: it is the
   coarse half of a chat's identity, so it is set small and quiet and the tile
   underneath carries the part that varies. */
.rail-day { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding: 8px 2px 3px; }
.rail-day:first-child { padding-top: 2px; }
/* The rename field, in the cog dialog and nowhere else. */
.tile-dlg-name { padding: 0 2px 4px; }
.tile-dlg-name-input { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); font-size: var(--fs-sm); padding: 6px 8px; }
.tile-dlg-name-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.session-box.pending { border-style: dashed; }

.tile-pending { display: flex; gap: 6px; margin-top: 7px; }
/* The name of a control on a tile -- "Workers", beside its pulldown. It wore
   `.tile-model-chip` and so read as a VALUE: that pill is the badge a model's
   name wears, and a label in one puts two pills on a row where only the right
   one is data. The pill comes off; the word stays.
   Sized for a rail tile and not for the dialog: `.tile-dlg-label` holds a fixed
   116px column because three rows there have to line up, and a 260px tile has
   no width to spare for a column of one. `flex: none` and no wrap, so the label
   takes exactly its own width and the pulldown keeps the rest.
   11px in the UI font rather than the chip's 10.5px monospace -- a word set in
   a text face reads larger at the same measure than one set in mono, and this
   is the smallest step at which a label is still comfortable. */
.tile-row-label {
	flex: none; align-self: center;
	font-size: var(--fs-2xs); color: var(--text-secondary);
	white-space: nowrap; cursor: pointer;
}
/* Asymmetric on purpose: the browser draws a select's arrow inside the padding
   box, so a symmetric 6px leaves it all but touching the border. The arrow side
   gets the air; the text side does not need it. This is the base every pulldown
   in the app inherits, the dialog's larger one included. */
.tile-model { flex: 1; min-width: 0; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-primary); font-size: var(--fs-xs); padding: 5px 18px 5px 6px; }
/* A pulldown, so `:focus-visible` -- see the note at `.settings-select`. */
.tile-model:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.tile-start { background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 5px 10px; font-size: var(--fs-xs); white-space: nowrap; }
.tile-start:hover { background: var(--accent-hover); }

.tile-active { display: flex; flex-direction: column; gap: 5px; margin-top: 7px; }
.tile-active-top { display: flex; align-items: center; gap: 6px; }
.tile-model-chip { font-family: var(--font-mono); font-size: var(--fs-3xs-2xs); color: var(--text-secondary); background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 20px; padding: 1px 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; }
/* Fold and Keep are pushed to the far end together, so the model chip keeps the
   near edge and the two acts sit as a pair. It was `.tile-fold` alone that
   carried the margin; with a second button beside it the rule has to be about
   whichever comes first, or Keep would be shoved away from Fold. */
.tile-active-top .tile-fold, .tile-active-top .tile-keep:first-of-type { margin-left: auto; }
.tile-active-top .tile-fold ~ .tile-keep { margin-left: 0; }
.tile-meter { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; min-width: 0; font-family: var(--font-mono); font-size: var(--fs-3xs-2xs); color: var(--text-muted); }
.tile-ctx { display: flex; align-items: center; gap: 4px; }
/* `position: relative` so the fold mark can be placed along it, and the overflow
   clip that keeps the fill inside the rounded ends keeps the mark inside too. */
.tile-ctx-bar { position: relative; width: 32px; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
.tile-ctx-fill { display: block; height: 100%; background: var(--accent); }
.tile-ctx-fill.high { background: var(--danger); }
/* Where the conversation folds. A hairline rather than a colour change, so the
   bar still reads as one quantity: the mark says where the threshold is, and
   the fill going `.high` says it has been passed. */
.tile-ctx-fold {
	position: absolute; top: 0; bottom: 0; width: 1px;
	background: var(--text-muted); opacity: 0.9;
}
.tile-ctx-pct { color: var(--text-secondary); }
.tile-tok { white-space: nowrap; }
.tile-cost { color: var(--text-secondary); white-space: nowrap; }
.tile-fold { background: none; border: 1px solid var(--border-strong); color: var(--text-secondary); border-radius: var(--radius-sm); font-size: var(--fs-2xs); padding: 2px 9px; white-space: nowrap; }
.tile-fold:hover { border-color: var(--accent); color: var(--accent-text); }
/* Keep as a Diamond. It wears the SAME shape as Fold because they sit side by
   side and a louder one would read as the recommended act -- which it is not;
   they answer different questions. What separates them is the word. */
.tile-keep { background: none; border: 1px solid var(--border-strong); color: var(--text-secondary); border-radius: var(--radius-sm); font-size: var(--fs-2xs); padding: 2px 9px; white-space: nowrap; }
.tile-keep:hover { border-color: var(--accent); color: var(--accent-text); }

/* Fold-into picker (a small anchored menu of Diamonds). */
.fold-menu { position: fixed; z-index: 1200; background: var(--bg-secondary); border: 1px solid var(--border-2); border-radius: var(--radius); box-shadow: 0 8px 28px rgba(0,0,0,0.4); padding: 5px; min-width: 180px; max-width: 260px; display: flex; flex-direction: column; gap: 2px; }
.fold-menu-head { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); padding: 4px 8px 2px; }
.fold-menu-empty { font-size: var(--fs-xs); color: var(--text-muted); padding: 2px 8px; }
.fold-menu-item { text-align: left; background: none; border: none; color: var(--text-primary); font-size: var(--fs-sm); padding: 7px 8px; border-radius: var(--radius-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fold-menu-item:hover { background: var(--bg-hover); }
.fold-menu-item.new { color: var(--accent-text); border-top: 1px solid var(--border); margin-top: 2px; }

/* The Chats section's own overflow -- "Delete all chats" and, later, whatever
   else is about the whole list rather than one tile. Same floating-popover
   shape as `.fold-menu` above, its own class only because it hangs off a
   different anchor and is not part of the fold feature. */
.railhead-menu { position: fixed; z-index: 1200; background: var(--bg-secondary); border: 1px solid var(--border-2); border-radius: var(--radius); box-shadow: 0 8px 28px rgba(0,0,0,0.4); padding: 5px; min-width: 170px; display: flex; flex-direction: column; gap: 2px; }
.railhead-menu-item { text-align: left; background: none; border: none; color: var(--text-primary); font-size: var(--fs-sm); padding: 7px 8px; border-radius: var(--radius-sm); white-space: nowrap; }
.railhead-menu-item:hover { background: var(--bg-hover); }
.railhead-menu-item.danger { color: var(--danger); }
.railhead-menu-item.danger:hover { background: var(--danger); color: var(--on-fill, #fff); }
.railhead-menu-item:disabled { color: var(--text-muted); cursor: default; }
.railhead-menu-item:disabled:hover { background: none; }

/* Global spend readout (session / week / month). */
.spend-row { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 2px; border-top: 1px solid var(--border); }
.spend-cell { flex: 1; display: flex; flex-direction: column; gap: 1px; align-items: center; padding: 2px 4px; border-radius: var(--radius-sm); }
.spend-label { font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.spend-amt { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); }
/* The spend governor's quiet "faster than usual" note: a full-width row
   below the three figures, warm amber, never shouting. It appears only when
   the live rate runs well above the user's own normal, and says nothing at
   all the rest of the time. */
.spend-governor { flex-basis: 100%; margin-top: 3px; padding: 3px 6px; border-radius: var(--radius-sm);
	font-size: var(--fs-3xs-2xs); text-align: center; letter-spacing: 0.01em; }
.spend-governor.amber { color: var(--warn); background: var(--warn-bg); }
.spend-governor.tripped { color: var(--danger); background: var(--warn-bg); font-weight: 600; }

/* User identity row (foot of the rail). */
.user-row { flex: 1; display: flex; align-items: center; gap: 7px; background: none; border: none; padding: 4px; border-radius: var(--radius-sm); overflow: hidden; text-align: left; }
.user-row:hover { background: var(--bg-hover); }
.user-avatar { color: var(--accent); font-size: var(--fs-sm); flex: none; }

/* Message copy button (revealed on hover).
   The gutter is the message's own bottom padding, and it has to hold two things:
   the button, and the air above it. The button is 22px tall and nothing else --
   a 14px glyph in 3px of padding inside a 1px border -- so a 22px gutter was
   filled to the pixel, and the top border of a filled control landed flush on
   the bottom edge of a filled bubble with no line between them. The two terms
   are kept apart here so that resizing the glyph cannot silently close the gap
   again. Only the air scales with the skin: the button is the same 22px under
   both, but the warm skin loosens every other gap in the chat, and a gap that
   stayed at 6 there would be the one place left crowded. */
.chat-msg { position: relative; }
.chat-msg { --copy-gutter: calc(22px + 6px * var(--space-scale, 1)); padding-bottom: var(--copy-gutter); }
.msg-copy { position: absolute; bottom: 0; background: var(--bg-tertiary); border: 1px solid var(--border-strong); color: var(--text-secondary); border-radius: var(--radius-sm); padding: 3px; line-height: 0; opacity: 0; transition: opacity 0.12s; }
.chat-msg-assistant .msg-copy { left: 0; }
.chat-msg-user .msg-copy { right: 0; }
.chat-msg:hover .msg-copy { opacity: 1; }
.msg-copy:hover { color: var(--text-primary); }
.msg-copy.done { color: var(--ok); border-color: var(--ok); }
.msg-copy .ic { width: 14px; height: 14px; }

/* ── Turns: collapse, select, fold ──────────────────────────────────────────
   A question is the switch for the answers beneath it, so it looks like one:
   it takes a pointer, and when it is closed it says so with an ellipsis rather
   than silently standing next to a gap. */
.chat-msg-user .chat-msg-content { cursor: pointer; }
.chat-msg-user.collapsed .chat-msg-content::after {
	content: ' ⋯';
	color: var(--accent-text);
	opacity: 0.55;
	font-weight: 600;
}

/* The tick that chooses a turn. It exists only in select mode, because outside
   it there is nothing it could mean.
   It centres on the BUBBLE, not on the box: the box carries the copy button's
   gutter below the bubble, and a plain 50% put the tick half a gutter low --
   beside the bottom edge of a one-line question rather than beside its middle. */
.turn-pick { position: absolute; right: 0; top: calc((100% - var(--copy-gutter, 0px)) / 2); transform: translateY(-50%); display: none; margin: 0; }
.turn-pick input { accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }
.chat-output.selecting .turn-pick { display: block; }
.chat-output.selecting .chat-msg-user { padding-right: 26px; }
.chat-output.selecting .chat-msg-user .msg-copy { right: 26px; }	/* out from under the tick */
.chat-msg-user.picked .chat-msg-content { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Header chips: a word, not a picture. `.on` is the latched − in select mode. */
.chip-btn {
	background: var(--bg-tertiary);
	border: 1px solid var(--border-strong);
	color: var(--text-secondary);
	border-radius: var(--radius-sm);
	font-size: var(--fs-xs);
	padding: 3px 8px;
	line-height: 1.5;
	white-space: nowrap;
}
.chip-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
.chip-btn.on { background: var(--accent); border-color: var(--accent); color: var(--on-fill, #fff); }
.chip-btn.accent { border-color: var(--accent); color: var(--accent); }
.chip-btn.accent:hover { background: var(--accent); color: var(--on-fill, #fff); }
.chip-btn.dim { opacity: 0.45; }
.csel { display: flex; align-items: center; gap: 4px; }

/* Select mode borrows the header, so the header lends it the room: the model
   picker and the running cost answer questions nobody is asking while choosing
   which turns to fold, and without this the chat's own name is crushed to "Ch…".
   Steps stands aside too — in select mode every turn is closed, so there are no
   tool steps on screen for it to show or hide. */
.chead.selecting .cmeter,
.chead.selecting #steps-toggle-btn,
.chead.selecting .chat-model-select { display: none; }

.pending-centre h2 { color: var(--text-primary); }

/* The Agents panel used to hide itself until a Diamond dispatched its first
   agent, and `body.agents-hidden` is what did it. The hiding was removed because
   a panel that comes and goes reads as a panel that was taken away -- the user
   asked where it had gone -- so it is now reachable at all times and says that
   nothing is running. Nothing sets the class any more, and these rules went with
   it: a selector nobody can match is a rule that will be read as live. The
   auto-reveal on first dispatch is unchanged and lives in daimond.js. */

/* Identity modal (create / unlock). */
#identity-modal .login-logo { height: 54px; margin: 0 auto 14px; display: block; }
/* The wordmark ships white-lettered. The script swaps it per theme too (via
   #id-logo), but CSS `content` applies at first paint and outranks the src
   attribute, so the light and candy themes are right before any script runs. */
:root[data-ink="dark"] #identity-modal .login-logo {
	content: url("../assets/daimond_word_dark.svg");
}
#identity-modal h2 { margin-bottom: 8px; }
#identity-modal .cfg-lead { margin: 0 0 12px; }

/* ── The front door ─────────────────────────────────────────────
   Three routes for a stranger, above the form, drawn only for a browser that
   has never held an identity (`syncDoors`, js/daimond.js).

   Its budget is HEIGHT. The create card already runs past a laptop's viewport
   and scrolls, and the form below it is what a returning stranger comes back
   for -- so this is a head, one line, and one wrapped row of three, and the
   row wraps rather than shrinking the names, because a route whose name is
   cut is a route nobody takes.

   The rule is `.id-doors`, not `#id-doors`, so a second host -- a landing
   panel, a phone sheet -- can wear the same strip without a new selector.
   `<a>` and `<button>` in the same row: two of the three leave the app and one
   raises a dialog, and the markup says which is which while `.id-door` makes
   them read as one set of choices. */
.id-doors {
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm);
	background: var(--bg-tertiary);
	padding: 9px 12px 10px;
	margin: 0 0 12px;
}
/* The question is the first half of the lead's own sentence, so it is set on the
   same line rather than as a head above it. Stacked, it cost a whole line for
   three words and the strip stood 176px tall -- which is what pushed the create
   button off the bottom of the card the night the strip was added. It keeps its
   own key and its own weight, so it still reads as the thing being asked. */
.id-doors-head { font-weight: 600; color: var(--text-primary); }
.id-doors-lead { font-size: var(--fs-xs); color: var(--text-secondary); margin: 0 0 7px; line-height: 1.45; }
.id-doors-row { display: flex; flex-wrap: wrap; gap: 6px; }
/* One appearance for a link and a button, so the three read as three routes
   rather than as two links and a control. `flex: 1 1 auto` with a floor lets
   two sit on a line and the third drop under them on a narrow card, which is
   what a 366px phone does with these three names. */
.id-door {
	flex: 1 1 auto; min-width: 8.5em;
	display: inline-flex; align-items: center; justify-content: center;
	text-align: center; text-decoration: none;
	background: var(--bg-secondary);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm);
	color: var(--text-primary);
	padding: 8px 10px;
	font-size: var(--fs-xs);
	line-height: 1.2;
	cursor: pointer;
}
.id-door:hover { border-color: var(--accent); }
.id-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* "I have a passcode" is a disclosure: it opens the note below it and closes it
   again. Held open, it wears the accent it wears under the pointer, so the
   second press is offered rather than guessed at. */
.id-door[aria-expanded="true"] { border-color: var(--accent); background: var(--accent-soft); }
/* The answer given to somebody who presses "I have a passcode" before there is
   a key on this device to redeem it onto. `--text-secondary` rather than a
   dimmed muted: it is the sentence that tells them what to do next. */
.id-doors-note { font-size: var(--fs-xs); color: var(--text-secondary); margin: 8px 0 0; line-height: 1.45; }
/* The account switcher on the unlock screen: whose passphrase to enter, or add another. */
.id-accounts { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.id-accounts-lead { font-size: var(--fs-xs); color: var(--text-muted); }
.id-account { display: flex; justify-content: space-between; align-items: center; gap: 10px; text-align: left; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-primary); padding: 9px 12px; font-size: var(--fs-sm); }
.id-account:hover { border-color: var(--accent); }
.id-account.on { border-color: var(--accent); background: var(--accent-soft); }
.id-account-name { font-weight: 600; }
.id-account-fp { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); }
.id-account-add { background: none; border: 1px dashed var(--border-strong); color: var(--text-secondary); border-radius: var(--radius-sm); padding: 8px 12px; font-size: var(--fs-sm); }
.id-account-add:hover { border-color: var(--accent); color: var(--text-primary); }

.id-fields { display: flex; flex-direction: column; gap: 10px; }
.id-fields input { background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 10px 12px; font-size: var(--fs-base); }
.id-fields input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
/* Unlocking: the name is not a field to fill but a statement of whose account is
   being opened. It stays a real input because that is what lets a password
   manager file the entry under a username, but it reads as information. */
.id-fields input[readonly] { color: var(--text-muted); border-style: dashed; }
.id-fields input[readonly]:focus { border-color: var(--border); }
/* The passphrase field carries a reveal toggle inside its right edge, so a long
   passphrase typed on a phone can be checked. The input reserves room for it. */
.pass-wrap { position: relative; display: flex; }
/* THE RESERVATION IS PART OF THE FIELD, and it has to survive a skin. Any skin
   that restates `padding` on inputs -- warm does, in `calc(9px * scale)
   calc(13px * scale)` -- wipes the right-hand reservation with the shorthand,
   and the typed passphrase then runs under the eye by ~17px at the default
   reading size. A skin cannot be expected to know which fields carry something
   inside them, so the field insists rather than the skin remembering. Measured
   with dev/probe_passeye.mjs, which reports the overlap in pixels. */
.pass-wrap input { flex: 1; padding-right: 40px !important; }
.pass-eye { position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
	width: 32px; height: 32px; padding: 0; display: flex; align-items: center;
	justify-content: center; background: none; border: none; color: var(--text-muted);
	cursor: pointer; }
.pass-eye:hover { color: var(--text-primary); }
.pass-eye .ic { width: 20px; height: 20px; }
/* ── The foot that holds the one next step ──────────────────────
   `.modal-card` scrolls (`overflow-y: auto`), and the create screen is the
   tallest thing that has ever been put in one: a logo, the stranger strip, a
   title, a lead, a name, a generated passphrase with its note and its
   acknowledgement, a passphrase field, a confirmation, and only then the button
   that does the thing. On 2026-08-14, at 1500x950, it ran 1086px inside an 872px
   box and "Create account" sat at 914-959 with the card's own edge at 912 --
   `elementFromPoint` over the middle of the button answered `#identity-modal`. A
   first-time visitor on a laptop could not see the button they had been sent
   here to press. Nor could one on a phone, and that half is older than the front
   door: at 375x812 the card was over its height by ~140px with the strip hidden
   altogether. `dev/l11_fold.mjs` measures both.

   So the foot STICKS to the bottom of the scroll port: whatever else is under
   the fold, the next step is on screen. Sticky and not a flex footer because the
   card is shared furniture -- a real footer means splitting every modal into a
   scrolling body and a fixed foot, and this is one screen's problem.

   It costs nothing when there is room. Sticky only lifts an element that would
   otherwise sit below the port, so on the UNLOCK screen -- half this height, no
   scrollbar -- the button stays exactly where it was. Its containing block is
   the form, so it can rise no further than the form's own top.

   The padding is the gap the button used to carry as `margin-top`. A margin
   would be transparent, and the form would scroll through the 12px band above a
   stuck button; padding on the foot paints the card's own ground there instead.
   `--bg-secondary` is that ground: `.modal-card` sets it and no skin restates
   it, so this follows the card through both skins and every palette.

   AND THE CARD'S BOTTOM PADDING BECOMES A SPACER, three rules below. A stuck
   element stops at the scroll port's CONTENT edge, while a scroll container's
   own end padding is scrollable area -- so the card's 24px (34px warm, 16px on
   a phone) was a band under the button that the acknowledgement row slid
   through, photographed on a 375px phone in the warm skin. Covering it from the
   foot cannot work: anything that reaches past the foot's box paints over
   "Choose my own passphrase instead" once the card is scrolled to the end and
   the foot is no longer stuck. Moving the same space to a `::after` puts the
   card's inner bottom edge and its content edge in the same place, so there is
   no band to cover, and the tail keeps the air it had. */
.id-foot {
	position: sticky; bottom: 0;
	background: var(--bg-secondary);
	padding: 12px 0 6px;
}
#id-primary { width: 100%; background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 10px 16px; font-size: var(--fs-lg); }
#id-primary:hover { background: var(--accent-hover); }
#id-primary:disabled { opacity: .5; cursor: not-allowed; }
#id-primary:disabled:hover { background: var(--accent); }

/* The generated passphrase. It is READ here, not typed, so it is set in the
   mono face at a size that survives being copied onto paper, and given room to
   wrap across three lines rather than scrolling sideways in a one-line box. */
/* The create screen carries more than any other modal — a name, the generated
   passphrase with its note and acknowledgement, the field, and two ways out — so
   it is given more of the window than the shared 80vh before it starts scrolling.
   A short window still scrolls, which is correct; this only stops a full-height
   one from hiding the button and the escape hatch under the fold. */
#identity-modal .modal-card { max-height: 92vh; padding-bottom: 0; }
/* The bottom padding, moved off the card and into its last line, so a stuck
   `.id-foot` has nothing scrolling underneath it (see the foot above). The
   number is the card's own: 24px, times `--space-scale` where a skin sets one --
   which is how skin-warm.css writes the very padding this replaces. An id beats
   both the skin's selector and the phone's, so this holds everywhere; a phone
   therefore ends 8px deeper than mobile.css's 16px, which is the one place this
   is not a straight swap and is below noticing at the end of a scroll. */
#identity-modal .modal-card::after {
	content: "";
	display: block;
	height: calc(24px * var(--space-scale, 1));
}
.pass-gen { background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 12px; display: flex; flex-direction: column; gap: 10px; }
.pass-gen-words { font-family: var(--font-mono); font-size: var(--fs-base); line-height: 1.6; color: var(--text-primary); word-spacing: .25em; overflow-wrap: break-word; }
.pass-gen-acts { display: flex; gap: 8px; }
.pass-gen-btn { flex: 1; background: none; border: 1px solid var(--border-strong); color: var(--text-secondary); border-radius: var(--radius-sm); padding: 6px 10px; font-size: var(--fs-xs); cursor: pointer; }
.pass-gen-btn:hover { border-color: var(--accent); color: var(--text-primary); }
.pass-gen-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }
.pass-gen-ack { display: flex; align-items: flex-start; gap: 8px; font-size: var(--fs-xs); color: var(--text-secondary); line-height: 1.4; cursor: pointer; }
.pass-gen-ack input { flex: none; margin-top: 1px; accent-color: var(--accent); }

/* The passkey button and its note are styled in passkey.css, which owns them. */

/* "Choose my own passphrase instead" — a way out of the generated phrase that
   is deliberately quieter than the button that accepts it. */
.id-choose { width: 100%; background: none; border: none; color: var(--text-muted); font-size: var(--fs-xs); padding: 10px 0 0; cursor: pointer; text-decoration: underline; }
.id-choose:hover { color: var(--text-primary); }

/* Chat input: grow to ~12 lines, scrollbar only on hover/focus. */
/* The placeholder and a single line of text sit CENTRED in the box. The old rule
   zeroed the bottom padding, which left all 9px of slack at the top and dropped the
   one line to the floor of the box — the "too low" placeholder. Splitting it 5/4
   keeps the same 32px resting height (21 line + 9 padding + 2 border) with the line
   in the middle, and the textarea still grows from there. */
#chat-input { max-height: 263px; padding-top: 5px; padding-bottom: 4px; overflow-y: hidden; }
#chat-input:hover, #chat-input:focus { overflow-y: auto; }
#chat-input::-webkit-scrollbar { width: 8px; }
#chat-input::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
#chat-input::-webkit-scrollbar-track { background: transparent; }


/* ── In-app dialogs ──────────────────────────────────────────────
   The replacements for window.prompt / confirm / alert. They sit on the
   same .modal / .modal-card furniture as Settings, so a destructive
   question looks like part of Daimond rather than an OS box quoting the
   origin over a dark UI. */
.dlg-card { width: 420px; }
/* `pre-wrap` and `anywhere` are both about the consent dialogs, which QUOTE the
   thing being authorised inside the sentence asking about it. The quote is no
   longer shortened, so two things have to hold however long it is: the blank
   lines the string was written with must survive, or the quote runs into the
   sentence and its edges cannot be seen; and an unbroken run -- a token, a card
   number, a base64 blob -- must wrap rather than overflow a 420px card and hide
   exactly the part worth reading. */
.dlg-msg { font-size: var(--fs-base); color: var(--text-secondary); line-height: 1.5; margin-bottom: 16px; white-space: pre-wrap; overflow-wrap: anywhere; }
.dlg-input { width: 100%; margin-bottom: 4px; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 9px 12px; font-size: var(--fs-base); }
/* `.dlg-input` is worn by the dialog's text fields AND by its pulldown
   (`.dlg-input.dlg-select`), so it takes `:focus-visible` for the pulldown's
   sake -- see the note at `.settings-select`. On the text fields it resolves to
   exactly what `:focus` did. */
.dlg-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
/* A models pulldown in a dialog: the same box as the text fields beside it, so the
   form reads as one form. A long model id is allowed to be long -- truncating it
   would hide the very part that tells two of them apart. */
.dlg-select { font-family: var(--font-mono); font-size: var(--fs-sm); }
/* The UI face, not --font-sans: that token does not exist, so the declaration
   was invalid and the group heading inherited the mono face of the options it
   was meant to stand apart from. */
.dlg-select optgroup { font-family: var(--font); font-style: normal; font-weight: 600; color: var(--text-secondary); }
.dlg-select option { font-family: var(--font-mono); color: var(--text-primary); }
.dlg-select option:disabled { color: var(--text-muted); }
/* A dialog's own tick: one thing the user turns ON, above the buttons. It is a
   `label` wrapping the box, so the words are part of the target -- a consent
   control whose only hit area is a 13px square is one people miss and then say
   they never chose. Unticked at every open, because the dialog is built fresh
   each time and a remembered tick would be the standing answer arriving by the
   back door again. */
.dlg-tick { display: flex; align-items: flex-start; gap: 9px; margin: 2px 0 14px; cursor: pointer;
	font-size: var(--fs-sm); color: var(--text-secondary); line-height: 1.4; }
.dlg-tick-box { margin: 1px 0 0; flex: none; accent-color: var(--accent); }
.dlg-tick:hover { color: var(--text-primary); }
.dlg-ok { background: var(--accent); color: var(--on-fill, #fff); border: 1px solid var(--accent); border-radius: var(--radius-sm); padding: 8px 16px; font-size: var(--fs-base); }
.dlg-ok:hover { filter: brightness(1.08); }
.dlg-err { min-height: 16px; font-size: var(--fs-xs); color: var(--danger); margin-bottom: 8px; }
/* The pair wraps rather than bursting. These two are `flex: 1` off a zero basis,
   but a one-word label cannot shrink below itself, so the row's real minimum is
   both labels plus four lots of side padding -- and the drawer that hosts the
   same form in the rail is about 190px wide, against a card's 420. "Add and
   sync" beside "Cancel" does not fit that, and the drawer clips what it cannot
   hold (`.admin-scroll` is overflow-x: hidden), so the primary button lost its
   right-hand end. Wrapping costs nothing where the row already fits: the buttons
   only take a line each when the alternative was losing one of them. */
.dlg-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.dlg-actions .dlg-cancel { margin-top: 0; flex: 1; }
.dlg-actions .dlg-ok { margin-top: 0; flex: 1; width: auto; }
.dlg-actions .dlg-ok.danger { background: var(--danger); border-color: var(--danger); color: var(--on-fill, #fff); }
.dlg-actions .dlg-ok.danger:hover { filter: brightness(1.1); }
.tile-fold.folded { color: var(--text-secondary); border-style: dashed; }

/* ── About ───────────────────────────────────────────────────────
   Inkscape's shape: the splash artwork owns the top of the card and reaches its
   edges, the identity sits under it, and the small print is a quiet row along
   the foot. It is the `.modal-card` / `.dlg-card` furniture every other dialog
   wears, with a class of its own for the parts that are only true here. */
/* `--about-pad` mirrors the card's own padding, and exists because the phone
   changes it (mobile.css drops `.modal-card` to 16px). Two bleeding children
   below cancel that padding out, and a bleed measured against the wrong number
   is a picture hanging over the edge of its card. */
.about-card { width: 480px; --about-pad: 24px; position: relative; }
/* The closer, over the splash rather than under it. Every other card in the app
   puts its cross in a title row above the content; this card's content STARTS
   with a full-bleed picture, so a row above it would push the artwork off the
   top edge and a row below it would put the way out halfway down the card.
   Absolute here and nowhere else: it is positioned against the card, and it
   scrolls with the picture it sits on, which is the behaviour that made
   absolute wrong everywhere the content is text.

   The heading in this row is `.vh` -- About is titled by its artwork -- so the
   row collapses to the cross and needs no width of its own. */
.about-card > .ui-head {
	position: absolute; top: 8px; right: 8px; z-index: 1;
	margin: 0; gap: 0;
}
/* The splash brings its own near-black ground, so the muted grey every other
   closer wears would sink into it. A dark disc under white ink reads on the
   artwork whatever the palette, because the artwork is fixed-colour too. */
.about-card > .ui-head .ui-close {
	background: rgba(0, 0, 0, 0.45);
	color: #fff;
	border-radius: 999px;
}
.about-card > .ui-head .ui-close:hover { background: rgba(0, 0, 0, 0.7); color: #fff; }
/* Full bleed. The card pads its contents and the splash is a picture, not a
   paragraph -- a margin round it reads as an accident.

   The width is the content box PLUS both paddings, which is exactly the padding
   box, so nothing overflows. That matters more than it looks: `.modal-card` is
   `overflow-y: auto`, and CSS computes `overflow-x: visible` to `auto` when the
   other axis is not visible -- so a splash one pixel wider than its padding box
   would hang a horizontal scrollbar under the picture on every open. */
.about-splash {
	display: block;
	width: calc(100% + var(--about-pad) * 2);
	/* Stated, not inherited: a splash squashed to a fixed height is the classic
	   way this goes wrong, and the artwork's own 863x448 is what decides it. */
	height: auto;
	margin: calc(var(--about-pad) * -1) calc(var(--about-pad) * -1) 0;
	border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}
.about-body { display: flex; flex-direction: column; align-items: center; gap: 12px; padding-top: 18px; }
/* The wordmark, at about twice the height the top bar draws it. `.brand-wordmark`
   is carried alongside for the light/dark swap, which is written against that
   class; the height here wins because it is the later rule. */
.about-word { height: 46px; width: auto; }
.about-said {
	font-size: var(--fs-base); color: var(--text-secondary);
	line-height: 1.55; text-align: center; margin: 0; max-width: 34em;
}
/* The version, said the way the rail's status panel says it: the same label
   word, the id in the mono face, and the app's one copy button beside it. */
.about-ver { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: var(--fs-xs); }
.about-ver-label { color: var(--text-muted); }
.about-build { font-family: var(--font-mono); color: var(--text-secondary); overflow-wrap: anywhere; }
/* The foot. A rule across the card, then the signature -- which is what the
   `100% + 48px` is doing again: the line spans the card, the badge does not. */
.about-maker {
	display: flex; align-items: center; justify-content: center;
	width: calc(100% + var(--about-pad) * 2);
	margin: 6px calc(var(--about-pad) * -1) 0;
	padding: 14px var(--about-pad) 2px;
	border-top: 1px solid var(--border);
}
/* ONE rule across the foot, not two. `.tile-dlg-foot` draws its own, which is
   right where the foot is the first thing under the body -- here the signature
   is already below a rule, and a second line 60px under the first read as a
   mistake in every palette. */
.about-card .tile-dlg-foot { border-top: none; margin-top: 10px; padding-top: 0; }
/* The maker's badge. One lozenge with its own white plate and its own border,
   so it takes no palette treatment and needs no dark/light twin -- it looks the
   same on every theme, which is the point of a mark. */
.made-by { position: relative; display: flex; align-items: center; flex: none; }
/* The two halves of the badge, laid over the artwork. Each is a real element
   with its own name, so a screen reader meets two claims rather than one
   picture, and each is a link: the flame to Oxedyne, the chip to the
   AI-disclosure mark it is claiming. `verify_badge.mjs` reads the 50% out of
   this rule and measures the artwork against it, so the two cannot drift. */
.made-by .mb-hit { position: absolute; top: 0; bottom: 0; width: 50%; display: block; }
.made-by .mb-oxedyne { left: 0; }
.made-by .mb-ai { right: 0; }
/* The whole plate dims under either half, because the artwork is one file and
   there is no half of it to dim on its own. Written on the parent rather than as
   `.mb-oxedyne:hover ~ img`, which was what stood here and matched nothing: the
   image is the FIRST child, so no sibling combinator can reach back to it. */
.made-by:has(.mb-hit:hover) img { opacity: 0.85; }
.made-by .mb-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.made-by img { height: 40px; width: auto; display: block; transition: opacity 0.15s ease; }

/* ── A tile's own dialog (the cog) ───────────────────────────────
   Same `.modal` / `.modal-card` furniture as every other dialog, so Escape,
   the backdrop and the focus trap behave the way they do everywhere. What is
   different is the foot: it holds Delete, and only Delete. The way out of the
   dialog is the cross in the title row, which is where a person looks for it;
   a full-width "Done" at the bottom of a scrolling card is a button you have to
   travel to in order to do nothing. */
.tile-dlg-head {
	font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em;
	color: var(--text-muted); margin: 14px 0 6px;
}
.tile-dlg-card h2 { margin-bottom: 4px; overflow-wrap: anywhere; }
/* The title row -- the Diamond's name, and the way out -- is `.ui-head` now;
   the element carries both classes so the selectors written against this one
   keep working. */
/* The closer. Its shape, its hover ground, its focus ring and its touch floor
   are all `.ui-close` now — this dialog was where the app's cross was worked
   out, and the rest of the app has since caught up with it. The class survives
   as the hook `shot_tiledlg` reaches for and as the place to say anything true
   of THIS cross alone. */
.tile-dlg-pause { display: flex; align-items: center; gap: 8px; }
.tile-dlg-pause-words { font-size: var(--fs-sm); color: var(--text-secondary); }
.tile-dlg-seg { display: flex; gap: 6px; }
/* The colour reset closes the section above it, so it needs the air a section
   break has. Without this it sat directly under the Text swatch and read as a
   third row of the same control. */
.tile-dlg-seg > .tile-dlg-clear { margin-top: 10px; }
/* One labelled row in the tile dialog -- a model, a colour, anything with a
   name on the left and a control on the right. `.tile-dlg-model` is the model
   rows' own name and is listed beside it rather than replaced: the two classes
   are emitted together, and either one alone still gets the row's shape. */
.tile-dlg-field,
.tile-dlg-model { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
/* The label column. A FIXED width, not a minimum: "Workers, images" is longer
   than "Daimon" and a min-width let it push its pulldown right, so three rows
   that are one setting read as three unrelated ones. The label ellipsises
   rather than moving the column it exists to line up.
   It is set in the UI font at the dialog's own reading size, in sentence case,
   and NOT in the small uppercase `.trig-label` wears. `.tile-dlg-head` -- the
   section heading directly above these rows -- is already 11px uppercase muted
   with the same letter-spacing, so a label drawn that way would be
   indistinguishable from the heading over it and "MODELS / DAIMON / WORKERS"
   would read as one flat list of small caps rather than a heading and the
   fields under it. Two levels want two treatments.
   `.tile-model-chip` is the selector the model rows used before they had a real
   <label>, and is kept here so the column survives either markup. That class is
   the badge a TILE wears to say which model is running -- monospace, a pill, a
   ground and a border -- because there it is a piece of data; in a form's label
   column it is a badge with nothing in it. The pill comes off either way. */
.tile-dlg-label,
.tile-dlg-model .tile-model-chip {
	flex: 0 0 116px; width: 116px;
	font-family: var(--font); font-size: var(--fs-sm);
	color: var(--text-secondary);
	background: none; border: 0; border-radius: 0;
	padding: 0; text-align: left;
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Only the real <label> claims to be pressable. The legacy span is not, and a
   span drawn with a pointer that no key can reach is what verify_a11y_keyboard
   calls a ghost. */
.tile-dlg-label { cursor: pointer; }
/* The pulldown is `.tile-model`, which is sized for a tile -- 12px type in a
   5px box, which is right in a 260px rail and undersized in a dialog whose
   other controls are 13px in a 8px box. It takes the dialog's measure here. */
.tile-dlg-field .tile-model,
.tile-dlg-model .tile-model {
	flex: 1 1 auto; min-width: 0;
	font-size: var(--fs-sm);
	/* Room on the right for the browser's own arrow. A native select draws it
	   inside the padding box, so with a symmetric 8px it sits all but touching
	   the border and reads as a rendering fault rather than a control. The left
	   stays at 8px: only the arrow side needs the air. */
	padding: 7px 22px 7px 8px;
	border-color: var(--border-strong);
}
/* A tile's colour. The browser draws the well itself, so the job here is to
   stop it drawing the CHROME around it: left alone, `input[type=color]` comes
   with a platform border and a grey plate that belong to no theme in this app.
   The element keeps the app's own border and radius and 2px of padding, which
   is what turns the native swatch into an inset chip rather than a flat block
   with a line round it. Both engines' inner pseudo-elements are named because
   Firefox and Chromium spell them differently and each ignores the other's. */
.tile-dlg-swatch {
	flex: none;
	width: 52px; height: 28px; padding: 2px;
	background: var(--bg-tertiary);
	border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
	cursor: pointer;
}
.tile-dlg-swatch::-webkit-color-swatch-wrapper { padding: 0; }
/* The FILL has to follow the frame's corner, less the 2px inset that separates
   them. Hard-coded at 2px it sat as a near-square inside a 6px radius -- and
   inside the warm skin's 10px, which is where it read as a mistake rather than
   as a detail. `calc()` keeps the two in step if the token ever moves. */
.tile-dlg-swatch::-webkit-color-swatch { border: 0; border-radius: calc(var(--radius-sm) - 2px); }
.tile-dlg-swatch::-moz-color-swatch { border: 0; border-radius: calc(var(--radius-sm) - 2px); }
.tile-dlg-swatch:hover { border-color: var(--accent); }
.tile-dlg-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tile-dlg-level {
	flex: 1;
	background: var(--bg-tertiary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); color: var(--text-secondary);
	padding: 8px 10px; font-size: var(--fs-sm);
}
.tile-dlg-level:hover { border-color: var(--accent); color: var(--text-primary); }
.tile-dlg-level[aria-pressed="true"] {
	background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text);
}
.tile-dlg-level:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* "Use the theme's colours": the way back from a colour that was chosen. It
   wears the same box as `.tile-dlg-level` and states it rather than inheriting
   it, so it does not depend on that class outliving the control it was written
   for. `flex: 0 0 auto` and not the segment's `flex: 1`: a segment divides its
   width between choices that are alternatives to each other, and a lone reset
   is not one of a set -- stretched across the row it would read as the row's
   main action, which is the opposite of what it is.
   It must stay BELOW `.tile-dlg-level`: an element carrying both classes takes
   whichever comes last, and `flex: 1` winning here is the whole defect. */
.tile-dlg-clear {
	flex: 0 0 auto;
	background: var(--bg-tertiary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); color: var(--text-secondary);
	padding: 8px 10px; font-size: var(--fs-sm);
}
.tile-dlg-clear:hover { border-color: var(--accent); color: var(--text-primary); }
.tile-dlg-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The daimon's "Change". Filled, because it IS the affirmative -- pressing it
   switches the model -- and it shows only while the pick differs, so it never
   sits idle beside the pulldown inviting a press that does nothing. Beside it the
   bordered `.tile-dlg-clear` reads as the quieter act it is. */
.tile-dlg-apply {
	flex: 0 0 auto;
	background: var(--accent); color: var(--on-fill, #fff); border: none;
	border-radius: var(--radius-sm); padding: 8px 12px; font-size: var(--fs-sm);
}
.tile-dlg-apply:hover { background: var(--accent-hover); }
.tile-dlg-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tile-dlg-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-top: 8px; }
/* The same note in the palette's warning colour, for the one that says a chosen
   pair of colours cannot be read. Colour ALONE, with no frame and no icon: it is
   a remark, not a refusal, and the sentence carries the whole meaning for a
   reader who sees no colour at all. The muted grey it replaces is deliberately
   the only thing that changes -- a bordered callout here would read as an error,
   and the choice is not an error. */
.tile-dlg-note.warn { color: var(--warn); }
/* Saving a template. The checkbox row reads left to right -- box then words --
   because it is a choice about what goes in, not a labelled field; the button
   below it takes its own width, since a control stretched across a dialog is
   the one a person presses without reading it. */
.tile-dlg-tmpl { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.tile-dlg-check { accent-color: var(--accent); width: 15px; height: 15px; flex: 0 0 auto; }
/* THE WORDS, NOT A LABEL COLUMN. `.tile-dlg-label` is 116px wide with an
   ellipsis, which is right beside a field it names and wrong for a checkbox
   whose whole meaning is in its own sentence: it drew "Include what it ha…".
   Here the words take the rest of the row and wrap. */
.tile-dlg-tmpl .tile-dlg-label {
	flex: 1 1 auto; width: auto;
	white-space: normal; overflow: visible; text-overflow: clip;
	cursor: pointer;
}
/* Saving is the SECTION's main action, not the dialog's, and directly below it
   sits Delete — filled, in the danger colour, and the one thing in here that
   cannot be undone. Two filled buttons a thumb apart is how the wrong one gets
   pressed, so this one wears a box rather than a fill. Its own rule and not
   `.tile-dlg-clear`'s, for the reason that class states about itself: a control
   borrowing another's styling depends on it outliving the control it was
   written for. */
.tile-dlg-tmpl-save {
	flex: 0 0 auto; align-self: flex-start; margin-top: 12px;
	background: var(--bg-tertiary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); color: var(--text-secondary);
	padding: 8px 14px; font-size: var(--fs-sm);
}
.tile-dlg-tmpl-save:hover:not(:disabled) { border-color: var(--accent); color: var(--text-primary); }
.tile-dlg-tmpl-save:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tile-dlg-tmpl-save:disabled { opacity: 0.55; cursor: default; }
.tile-dlg-say[hidden] { display: none; }

/* The foot. Delete takes what it needs and no more -- it is the one thing in
   here that cannot be undone, and a button stretched across the width of a
   dialog is the button a person presses without reading it. Where a dialog
   still ends in Done (`dialog()` builds one for the chat tile and others), Done
   takes the rest of the row and Delete keeps its own width beside it. */
.tile-dlg-foot { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 14px; }
.tile-dlg-foot .tile-dlg-delete { flex: 0 0 auto; }
.tile-dlg-foot .tile-dlg-done { flex: 1; }


/* Locked: lockApp() empties the panels of the user's content, so the shell
   is left standing but bare. Nothing of theirs is on screen behind the card. */
body.locked .tile-fold, body.locked .spend-row { display: none; }

/* ── The account's own controls ──────────────────────────────────
   These were a floating menu anchored to the user row. They live in the Admin
   panel's home view now: the device fingerprint is worth seeing, not worth a
   popup. */
/* The fingerprint, with the button that copies it. A flex row rather than a
   block, so the button keeps its place while the value takes whatever width is
   left: as a block, the value's own ellipsis ate the button on a narrow rail. */
.account-fp { display: flex; align-items: center; gap: 2px; font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-muted); padding: 0 8px 8px; }
.account-fp-val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The devices that sync this account. A row is a line of text with two quiet
   controls on it: name it, or take it off the list.
   Removing is not revoking, and the styling should not imply that it is.
   Pairing hands the second device the SAME keypair, so nothing here can lock a
   device out; the cross tidies a register of what has been seen. It is drawn as
   the milder of the two acts on the row -- a danger colour on hover, and no
   ground of its own -- rather than as a destructive button. */
.device-row { display: flex; align-items: baseline; gap: 6px; padding: 5px 8px; font-size: var(--fs-sm); color: var(--text-primary); }
/* The name shrinks first and the id takes the slack, so the id sits with the
   name it belongs to and the last-seen column stays flush right. */
.device-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.device-id { flex: 1 1 auto; font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-muted); }
.device-when { flex: none; font-size: var(--fs-2xs); color: var(--text-muted); white-space: nowrap; text-align: right; }
/* Naming a device, and removing one. Muted until the row is under the pointer
   or the button has the keyboard, so a column of pencils does not compete with
   the names. The tap target stays 24px square whatever the reading size.
   Off the row's baseline, and only this one: three sizes of text SHOULD share a
   baseline, which is what the row asks for, but a 24px box has no business on
   one. Baseline-aligned it hung by the pencil's own baseline -- so the box
   overshot the line top and bottom, taking the row's height with it, and where
   the glyph sat depended on which font the platform found '✎' in. Centred, the
   square is the row's and the glyph can come from anywhere.
   The type size is LARGER than the row it sits on, which for a word would be
   wrong and for this glyph is what makes it legible: '✎' is a dingbat and is
   drawn well inside its em box, so at the row's own 13px the pencil came out
   around 8px of actual ink and read as a smudge. 18px of box gives it roughly
   the ink a 13px letter has. The 24px target is untouched. The remove cross
   takes the identical box: two controls of different sizes side by side on
   every row would be a worse fault than either of them being wrong. */
.device-rename, .device-remove { flex: none; align-self: center; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; margin-left: 2px; border: 0; border-radius: 6px; background: transparent; color: var(--text-secondary); font-size: var(--fs-2xl); line-height: 1; cursor: pointer; opacity: .75; }
.device-row:hover .device-rename, .device-rename:hover, .device-rename:focus-visible,
.device-row:hover .device-remove, .device-remove:hover, .device-remove:focus-visible { opacity: 1; color: var(--text-primary); background: var(--bg-hover); }
/* Removing a device is not renaming it, and the colour is the only thing on the
   row that says so. The row-hover selectors are repeated with the button's own
   state on the end: `.device-row:hover .device-remove` is more specific than a
   bare `.device-remove:hover`, so without them the neutral colour above would
   win at exactly the moment the pointer is on the cross. */
.device-remove:hover, .device-remove:focus-visible,
.device-row:hover .device-remove:hover,
.device-row:hover .device-remove:focus-visible { color: var(--danger); }

#id-name-row { margin-bottom: 8px; }
#id-name { width: 100%; }


/* ── Agent tiles ─────────────────────────────────────────────────
   A dispatched worker: what it was told to do, what it is doing, what it
   cost, and a control to stop it or fold its summary into the crystal. */
.atask { font-size: var(--fs-2xs); color: var(--text-secondary); line-height: 1.35; margin: 4px 0 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.acard.queued { opacity: 0.7; }
.acard.stopped { opacity: 0.75; }
.acard.paused { opacity: 0.85; border-color: var(--border-2); border-left: 2px solid var(--accent); }
.pill.queued { background: var(--bg-tertiary); color: var(--text-secondary); }
.pill.stopped { background: var(--bg-tertiary); color: var(--text-secondary); }
.pill.paused { background: var(--accent-soft); color: var(--accent-text); }
.aacts { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
.afolded { flex: 1; font-size: var(--fs-2xs); color: var(--text-muted); font-family: var(--font-mono); }
.abtn { flex: 1; font-size: var(--fs-2xs); padding: 4px 6px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); }
.abtn:hover { background: var(--bg-hover); }
.abtn.stop, .abtn.a-cross { color: var(--danger); }
.abtn.stop:hover, .abtn.a-cross:hover { background: var(--danger); color: var(--on-fill, #fff); border-color: var(--danger); }
.abtn.a-play { color: var(--accent-text); }
.abtn.a-play:hover { background: var(--accent); color: var(--on-fill, #fff); border-color: var(--accent); }

/* Agents panel header: pause / resume / stop, acting on every agent. The one
   brake on a runaway fan-out, so the group is prominent but quiet until there
   are agents to act on. */
.agents-ctl { display: inline-flex; gap: 3px; align-items: center; }
.agc { width: 22px; height: 22px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--bg-secondary); color: var(--text-secondary); }
.agc:hover:not(:disabled) { color: var(--text-primary); }
.agc:disabled { opacity: 0.3; cursor: default; }
#agents-stop:hover:not(:disabled) { color: var(--danger); border-color: var(--danger); }
#agents-play:not(:disabled) { color: var(--accent-text); border-color: var(--accent); }
/* When the pool is held, the resume control pulses so the paused state is not missed. */
.agents-ctl.holding #agents-play { background: var(--accent-soft); }
.agents-stat { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); padding: 0 6px 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.diff-accept:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
.diff-accept:disabled:hover { filter: none; }


/* ── Crystal: edit and history ─────────────────────────────────────
   Every version was already snapshotted to disk; nothing could reach one.
   An accepted fold overwrites the crystal, so without this there is no undo. */
.crystal-bar { display: flex; gap: 6px; margin-bottom: 10px; }
.crystal-act { font-size: var(--fs-xs); padding: 4px 10px; background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); }
.crystal-act:hover { background: var(--bg-hover); color: var(--text-primary); }
.crystal-act.primary { background: var(--accent); color: var(--on-fill, #fff); border-color: var(--accent); }
.crystal-edit { width: 100%; min-height: 320px; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 12px; font-family: var(--font-mono); font-size: var(--fs-sm); line-height: 1.6; resize: vertical; }
.crystal-edit:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }

/* ── The crystal as a page: the chrome around the frame ────────────
   The frame, the built-in view that replaces a page which fails, and the note
   that says it failed are all drawn by css/crystal.css. What is here is what
   the APP puts around them -- the ask row, the ✎ form -- and not one class the
   frame lane also names. crystal.css is linked after this file anyway, so it
   would win on anything the two ever repeated. */

/* App chrome, BELOW the frame, never inside it. A page cannot ask the user
   anything -- a parent cannot verify user activation across the boundary, so a
   timer there is indistinguishable from a click -- so the box that talks to the
   daimon about the page lives out here, where a click is provably a person's. */

/* The memory as it sits on disk, shown when crystal.js did not load at all. A
   broken build must not look like an empty Diamond. */
.crystal-raw { white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); margin: 0; }

/* The ✎ form. A textarea written straight through was harmless for markdown,
   which has no parse failure, and a data-loss path for JSON. */
.crystal-form { display: flex; flex-direction: column; gap: 4px; }
.crystal-form-row { display: flex; flex-direction: column; }
.crystal-form-list { display: flex; flex-direction: column; gap: 6px; }
.crystal-form-list > .crystal-act { align-self: flex-start; }
.crystal-form-item { display: flex; flex-direction: column; gap: 4px; padding: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.crystal-form-line { display: flex; gap: 6px; align-items: center; }
.crystal-form-input { flex: 1; min-width: 0; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 7px 10px; font-size: var(--fs-sm); }
.crystal-form-item .crystal-form-input { background: var(--bg-primary); }
.crystal-form-area { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 8px 10px; font-size: var(--fs-sm); line-height: 1.6; resize: vertical; }
.crystal-form-item .crystal-form-area { background: var(--bg-primary); }
.crystal-form-input:focus, .crystal-form-area:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.crystal-form-input::placeholder, .crystal-form-area::placeholder { color: var(--text-muted); }
.crystal-form-x { flex: none; padding: 2px 8px; line-height: 1.4; }

/* Read-only, and written back exactly as it came in. A form that drops what it
   cannot render is the defect this design is shaped around. */
.crystal-form-extra { margin-top: 12px; padding: 10px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.crystal-form-extra pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary); }

/* The raw JSON, behind a second click, and it refuses to save unparseable text. */
.crystal-json { width: 100%; min-height: 320px; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 12px; font-family: var(--font-mono); font-size: var(--fs-sm); line-height: 1.6; resize: vertical; }
.crystal-json:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.hist-list { display: flex; flex-direction: column; gap: 6px; }
/* Wraps, so a note can take a line of its own beneath the version and its
   buttons rather than squeezing between them. A row with no note is unchanged:
   two children, one line. */
.hist-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 10px; padding: 8px 10px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.hist-note { flex: 1 0 100%; font-size: var(--fs-2xs); color: var(--text-muted); line-height: 1.5; overflow-wrap: anywhere; }
.hist-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.hist-ver { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-primary); }
.hist-kind { font-size: var(--fs-xs); color: var(--text-secondary); }
.hist-when { font-size: var(--fs-2xs); color: var(--text-muted); }
.hist-acts { display: flex; gap: 6px; flex: none; }

/* ── Tags ────────────────────────────────────────────────────────
   The user's filing system for Diamonds, and nothing more: a tag never reaches a
   model, a crystal or a prompt. A chip's hue is hashed from the tag name in JS
   and arrives as --tag-h, so one tag is one colour everywhere; saturation and
   lightness come from the theme, so the dark theme gets a deep chip with light
   lettering and the light themes get the reverse, and neither has to know
   which tags exist. */
.tag-chip { display: inline-flex; align-items: center; gap: 3px; max-width: 110px; font-family: var(--font); font-size: var(--fs-3xs); line-height: 1.6; padding: 0 6px; border-radius: 20px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
	background: hsl(var(--tag-h, 0) 30% 20%); color: hsl(var(--tag-h, 0) 55% 78%); border: 1px solid hsl(var(--tag-h, 0) 28% 34%); }
button.tag-chip:hover { background: hsl(var(--tag-h, 0) 34% 27%); border-color: hsl(var(--tag-h, 0) 40% 48%); }
:root[data-ink="dark"] .tag-chip { background: hsl(var(--tag-h, 0) 70% 94%); color: hsl(var(--tag-h, 0) 55% 30%); border-color: hsl(var(--tag-h, 0) 45% 80%); }
:root[data-ink="dark"] button.tag-chip:hover { background: hsl(var(--tag-h, 0) 74% 88%); border-color: hsl(var(--tag-h, 0) 50% 66%); }

/* On a Diamond box, beside v{n} and the time. The +N stands for the tags that
   did not fit and names them on hover.

   Shaped like the chips it stands for, in a neutral. It was bare monospace grey
   beside three coloured pills, and when the meta row wrapped it landed alone on
   the second line under `v0` and read as debris rather than as "and two more
   tags". Same pill, same size, no hue — it is not a tag, it is a count of
   them. */
.tag-more {
	display: inline-flex; align-items: center;
	font-family: var(--font); font-size: var(--fs-3xs); line-height: 1.6;
	padding: 0 6px; border-radius: 20px; white-space: nowrap;
	background: var(--bg-tertiary); color: var(--text-muted);
	border: 1px solid var(--border); cursor: default;
}
.tag-active { padding-right: 3px; }
/* The closer owns the pill's right end cap: a full-height square flush with
   the edge, so its centre falls exactly on the cap's centre (half the chip's
   height in from the right), whatever size the chip is. The chip's own right
   padding is dropped when a closer is present -- the square supplies it. */
.tag-x {
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	aspect-ratio: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	/* The flex centring centres the glyph's TEXT BOX, but the × ink rides
	   between baseline and x-height and hugs the left of its advance --
	   measured 0.5px low and 0.22px left at 13px. This puts the INK at the
	   exact centre of the pill's right end cap, in em so it scales. */
	transform: translate(0.017em, -0.038em);
	background: none;
	border: none;
	color: inherit;
	font-size: var(--fs-sm);
	line-height: 1;
	padding: 0;
	opacity: 0.65;
	cursor: pointer;
}
.tag-x:hover { opacity: 1; }
/* The pool chip's closer deletes the TAG, so it reads as the danger it is. Its ×
   is drawn here rather than set as the button's text: the chip's textContent is
   the tag name, and everything that reads a chip reads that. */
.tag-kill::after { content: '\00d7'; }
.tag-kill:hover { color: var(--danger); opacity: 1; }
.tag-chip:has(.tag-x) { position: relative; padding-right: 2em; }
.tag-editor .tag-chip:has(.tag-x) { padding-right: 2em; }

/* A tag the rail is currently filtering ON, seen on the Diamond boxes that
   survived it. One neutral chip, the same in every palette and for every tag:
   the hue says WHICH tag, so it cannot also say which tag is working. A ring in
   the tag's own colour said it too quietly -- a 1px outline one hue-step from
   the chip it surrounds is not a state a rail can be read at a glance.

   Deliberately NOT a box-shadow. A ring drawn outside the border box takes no
   layout space, so the pool and the active row -- both `overflow-y: auto` and
   both sized to the exact height of one chip row -- sliced it off top and
   bottom. That is the clipping in the report: a scroll box cutting ink that was
   never in its layout. Ink that has to survive belongs inside the border box.

   The selector carries `[data-ink]` because the per-ink chip rules above
   are three classes deep; a bare `.tag-inc` loses to them on every theme but
   the default, and the whole point of this state is that it does not vary. */
:root[data-ink] .tag-chip.tag-inc {
	background: var(--tag-on-bg);
	border-color: var(--tag-on-bg);
	color: var(--tag-on-fg);
}
:root[data-ink] button.tag-chip.tag-inc:hover {
	background: var(--tag-on-hover);
	border-color: var(--tag-on-hover);
}

/* A tag the rail is REFUSING. The negation sign is drawn here rather than set as
   the button's text -- a chip's textContent is the tag name, and the rail, the
   filter and the search all read it -- and the fill comes off with it, so a
   refused tag is told from a wanted one across the rail rather than by reading
   it. Same hue, because it is the same tag. */
.tag-no::before { content: '\00ac'; font-weight: 700; }
.tag-no { background: transparent; border-style: dashed; color: hsl(var(--tag-h, 0) 30% 62%); }
button.tag-no:hover { background: hsl(var(--tag-h, 0) 30% 16%); border-color: hsl(var(--tag-h, 0) 40% 48%); }
:root[data-ink="dark"] .tag-no { background: transparent; color: hsl(var(--tag-h, 0) 35% 40%); }
:root[data-ink="dark"] button.tag-no:hover { background: hsl(var(--tag-h, 0) 60% 96%); }

/* All / Any: how two or more wanted tags combine. Both words are shown with one
   lit rather than a single label that flips, because a lone word cannot say
   whether it is naming the state or the action. It appears only when there are
   two tags for it to combine. */
.tag-mode { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 20px; overflow: hidden; }
.tag-mode-btn { display: inline-flex; align-items: center; justify-content: center; font-family: var(--font); font-size: var(--fs-3xs); line-height: 1.6; padding: 0 7px; background: none; border: none; color: var(--text-muted); cursor: pointer; }
.tag-mode-btn:hover { color: var(--text-primary); }
.tag-mode-btn.on { background: var(--accent); color: var(--bg-primary); }

/* Put the whole filter down. Deliberately not a chip: it is not a tag. */
.tag-clear-all { font-family: var(--font); font-size: var(--fs-3xs); line-height: 1.6; padding: 0 4px; background: none; border: none; color: var(--text-muted); cursor: pointer; text-decoration: underline; }
.tag-clear-all:hover { color: var(--text-primary); }

/* The tag editor, reached from the crystal bar. */
.tag-editor { display: flex; flex-direction: column; gap: 10px; max-width: 460px; }
.tag-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }
.tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-height: 20px; }
.tag-none { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }
.tag-add { display: flex; gap: 6px; }
/* No `max-width`. `flex: 1` already sizes it against the "+ Add" beside it, and
   the cap on top of that stopped the row 116px short of a 376px card while every
   chip row above it ran the full width — one row in the editor that did not
   reach the edge the rest reach. */
.tag-input { flex: 1; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); font-size: var(--fs-sm); padding: 4px 8px; }
.tag-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.tag-input::placeholder { color: var(--text-muted); }
/* The two boxes: what is on the Diamond, and the pool everything comes from.
   The add input sits inside the pool's box, because typing mints into it. */
.tag-box {
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	background: var(--bg-secondary);
	padding: 8px 10px 10px;
	display: flex;
	flex-direction: column;
	gap: 8px;
}
.tag-box-label { font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.tag-editor .tag-chip { font-size: var(--fs-xs); padding: 2px 9px; }


/* ── Credits ─────────────────────────────────────────────────────
   Buying credits is a plain one-time purchase: four amounts, no plans, no
   auto-renew. The balance sits beside the spend figures, where the user
   already looks for what things cost. */
.credits-balance { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text-primary); margin-bottom: 10px; }
/* Inserted between the balance and the pack buttons; without its own bottom
   margin it sat flush on the packs row. */
#credits-see-spend { margin: 2px 0 12px; }
.credits-packs { display: flex; gap: 8px; }
/* `.settings-section button` is accent-filled, which would make four money
   buttons shout in red. Money is stated, not sold: quiet by default, and the
   accent kept for hover, where it means "this is the one you are choosing". */
.settings-section .credit-pack { flex: 1; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 6px; font-size: var(--fs-base); font-family: var(--font-mono); }
.settings-section .credit-pack:hover { background: var(--bg-hover); border-color: var(--accent); color: var(--text-primary); }

/* ── Pro, at the top of Credits ───────────────────────────────────
   The one deliberate sell in the drawer: owning Daimond. It gets a bordered
   card so it reads as an offer rather than a note, and its button IS accent
   filled -- unlike the quiet money buttons below, this is the action the panel
   is steering toward, and there is only one of it. Owned, it becomes a calm
   confirmation with no button at all. */
.pro-block { margin-bottom: 14px; }
.pro-block:empty { margin: 0; }
.pro-offer, .pro-owned { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; background: var(--bg-secondary); }
.pro-offer p { margin: 0 0 8px; font-size: var(--fs-base); color: var(--text-primary); line-height: 1.45; }
.pro-offer .pro-fine { font-size: var(--fs-sm); color: var(--text-secondary); }
.pro-buy { width: 100%; margin-top: 4px; padding: 10px; background: var(--accent); color: var(--accent-fg, #fff); border: none; border-radius: var(--radius-sm); font-size: var(--fs-base); font-weight: 600; cursor: pointer; }
.pro-buy:hover:not(:disabled) { filter: brightness(1.08); }
.pro-buy:disabled { opacity: 0.6; cursor: default; }
.pro-err { margin-top: 6px; font-size: var(--fs-sm); color: var(--danger, #c0392b); }
.pro-owned { color: var(--text-primary); font-size: var(--fs-base); line-height: 1.45; }

/* ── Credits, in the Models panel ─────────────────────────────────
   Credits are a provider row like any other, with one difference the user must
   see before they pick and not after: this row spends money they are holding
   HERE, and every other row spends money they hold with somebody else. So the
   row is tinted, its balance sits on its name, and each of its models carries
   the word. The rest stay unmarked, which is how a reader knows they are not
   this — except where two rows serve one model name, where both say whose is
   whose, because there the name alone cannot. Nothing shouts: the accent is a
   quiet edge, not a banner. Money is stated, not sold. */
/* No per-theme rules below: every colour is a variable, so the tint follows the
   theme the way the rest of the panel does, and a fourth theme needs nothing
   here. Specificity, not order, is what beats models.css's plain head. */
.models-prov.paid { border-color: var(--accent-soft); }
.models-prov.paid .models-prov-head { background: var(--accent-soft); }
.models-prov.paid .models-prov-name { color: var(--accent-text); }

/* Name, balance and host are three units on one line that cannot hold three:
   the rail is 220px, and this row must survive it. So they wrap as WHOLES --
   the break falls between facts, never through one. "$8.40 / left" puts a
   number on one line and its meaning on the next, and "Daimond / credits"
   hyphenates the product. */
.models-prov .models-prov-name { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 7px; }
.models-nm  { white-space: nowrap; }
/* A balance is a fact about the row, not a status, so it rides on the name. */
.models-bal { font-family: var(--font-mono); font-weight: 400; font-size: var(--fs-2xs); color: var(--text-primary); white-space: nowrap; }
/* Whose machine actually runs it. Subdued, and never the headline: the user
   bought Daimond credits, not an OpenRouter account -- but they are still owed
   the name of the company their words are sent to. */
.models-via { font-weight: 400; font-size: var(--fs-3xs); color: var(--text-muted); white-space: nowrap; }

/* The economy a model belongs to, on the model. `credits` draws down the
   balance; `your key` marks a model whose name also appears on another row, so
   the pair is never a coin toss over who gets paid. */
.models-econ { flex: none; font-family: var(--font); font-size: var(--fs-4xs); letter-spacing: 0.03em; text-transform: uppercase; padding: 1px 5px; border-radius: 20px; border: 1px solid var(--border-2); color: var(--text-muted); background: var(--bg-tertiary); }
.models-econ.paid { border-color: var(--accent-soft); color: var(--accent-text); background: var(--accent-soft); }
.models-model.on .models-econ.paid { border-color: var(--accent); }

/* A model row and the button that copies its id. The row stays the button it
   was -- clicking it stars the model as the default -- and the copy sits beside
   it rather than in it, since a button cannot hold another one.
   `align-items: flex-start` for the same reason `.models-model` uses it: an id
   is `word-break: break-all` in a column about 90px wide once the star and the
   chips have taken their share of the rail, so two and three line rows are
   normal here and the copy belongs on the id's first line. */
.models-modelrow { display: flex; align-items: flex-start; gap: 2px; }
.models-modelrow > .models-model { flex: 1 1 auto; min-width: 0; }
.models-modelrow > .copy-id { align-self: flex-start; margin-top: 3px; }
.models-modelrow:hover .copy-id { opacity: 1; }

/* The gateway's own words about why it cannot sell inference. It writes better
   advice than this panel can ("bring your own model key to keep working"), so
   it is shown rather than flattened into a one-word state. */
.models-why { padding: 6px 8px 2px; font-size: var(--fs-2xs); line-height: 1.5; color: var(--text-secondary); }
.settings-section .credit-pack:disabled { opacity: 0.5; }
/* A pack in a non-USD display currency: the round local figure leads, the
   exact billed dollars recede beside it. */
.credit-pack .pack-local  { font-weight: 600; }
.credit-pack .pack-billed { opacity: 0.72; font-size: var(--fs-2xs); }
/* The billing sentence under the pack grid spans the full row of the flex
   container it sits in. */
.credits-packs .pack-note {
	flex: 1 1 100%;
	font-size: var(--fs-2xs);
	color: var(--text-muted);
	line-height: 1.45;
	margin-top: 2px;
}
/* The billing note after the Pro buy button breathes like the fine print
   above it. */
.pro-offer .pro-fine + .pro-fine, .pro-offer button + .pro-fine { margin-top: 8px; }
/* Credits are a BALANCE, not a spend period, so they take their own line rather
   than squeezing in as a fourth column and clipping the rail. */
.credit-cell { flex-basis: 100%; flex-direction: row; align-items: baseline; justify-content: space-between; margin-top: 4px; padding-top: 5px; border-top: 1px dashed var(--border); }
.credit-cell .spend-amt { color: var(--text-primary); }

/* A compiled document: a stage panel, wide enough to actually read. */
.doc-name { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; }


/* ── Workspace: organise it ──────────────────────────────────────
   The tree was read/write only: no rename, no move, no new folder, no
   upload, and no way to find anything once it grew past a screenful. */
.files-filter { padding: 6px 8px 0; }
.files-filter-input { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); padding: 5px 8px; font-size: var(--fs-xs); }
.files-filter-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.files-filter-input::placeholder { color: var(--text-muted); }
.files-ren { color: var(--text-secondary); }
.files-ren:hover { color: var(--accent); }

/* The standing-instructions chip: quiet, but visible, so the user knows the
   rules are in force and can open them. */
.instructions-chip { display: inline-block; margin: 0 8px 6px; font-family: var(--font-mono); font-size: var(--fs-2xs); padding: 3px 8px; background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 999px; }
.instructions-chip:hover { color: var(--accent); border-color: var(--accent); }

/* A readable multi-line dialog body: a version preview that collapses its
   newlines into one run-on line cannot be read, which defeats the History
   screen entirely. */
.dlg-pre { max-height: 45vh; overflow: auto; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.55; color: var(--text-secondary); background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 14px; }

/* N5: the modal's scrollbar rendered as a bright light track on the dark card —
   on the first screen a new user sees. */
.modal-card::-webkit-scrollbar, .dlg-pre::-webkit-scrollbar { width: 10px; }
.modal-card::-webkit-scrollbar-track, .dlg-pre::-webkit-scrollbar-track { background: transparent; }
.modal-card::-webkit-scrollbar-thumb, .dlg-pre::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 999px; border: 3px solid transparent; background-clip: content-box; }
.modal-card::-webkit-scrollbar-thumb:hover, .dlg-pre::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: content-box; }


/* ── The three zones ─────────────────────────────────────────────
   Rail (left, permanent) | Stage (middle, exclusive) | Dock (right, tagged).
   The stage seats as many panels as its width will carry, up to four, so you
   never have to leave the conversation to do a thing: the message, the page,
   the document and the pages it was typeset into — each opens BESIDE the
   daimon, not over it. How many is derived from the width; see `stageMax` in
   daimond.js, and the measurement beside `MIN_W`. */
.stage { display: flex; flex: 1; min-width: 0; min-height: 0; gap: 0; }
/* `min-width: 0` so a seat is the width the engine gave it. A flex item's
   automatic minimum is its content's, so without this a seat asked for 380px
   could quietly take more and push the seat beside it off the stage. */
.stage > .panel { min-height: 0; min-width: 0; }
.dock { display: flex; gap: 8px; min-width: 0; min-height: 0; flex: none; }
.pcol { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0; flex: 1; }
.pcol > .panel { flex: 1; min-height: 0; width: auto !important; }
.pcol:empty { display: none; }
/* The divider between two panels stacked in a column: the horizontal twin of
   .phandle. It STANDS IN FOR the column's gap rather than adding to it — a
   handle placed between panels that were already 8px apart put 26px of nothing
   between two cards — so a stacked column gives its gap up to the handles. */
.pcol.stacked { gap: 0; }
.hstack { height: 16px; flex: none; cursor: row-resize; position: relative; touch-action: none; }
/* STANDING, AND THE FULL WIDTH, exactly as the rail's own `.rhandle` stands
   between Chats and Diamonds.

   It used to be transparent at rest and inset a tenth either end, on the
   reading that the panels either side "carry their own borders, so a line at
   rest would be a third edge in the gap". They do not. A border and a radius
   are given to `.ai, .web, .doc, .msg, .compose, .tools` -- the STAGE's cards.
   Everything that seats in the dock (`.rail, .agents, .work, .mail, .spend`) is
   a bare column with no edge of its own, so this line was not a third edge, it
   was the only one -- and a divider that appears under the pointer is a
   divider nobody finds, since finding it is what the pointer was looking for.
   Reported as: several panels in one dock column could not be told apart.

   The rail's rule is copied rather than adapted, so the two dividers are one
   idea: `--border` at rest, `--accent` under the pointer or in a drag. Only the
   offset differs, and only because this handle is 16px tall against the rail's
   10px. */
.hstack::after { content: ""; position: absolute; left: 0; right: 0; top: 7px; height: 2px; border-radius: 2px; background: var(--border); transition: background 0.15s; }
.hstack:hover::after, .hstack.dragging::after { background: var(--accent); }

/* ── The Web panel ───────────────────────────────────────────────
   A page can always be SHOWN. It can be operated only where a driver can
   reach it: a same-origin page we serve ourselves, or a real tab under
   Daimond Hands. A cross-origin iframe is opaque to us — that is the
   same-origin policy, not a gap to be worked around. */
/* A COLUMN, so a standing strip can sit above the picture rather than beside
   it. It held one flexible child until `.web-live` arrived, so the direction
   made no difference before and makes one now; the three things that cover this
   body -- the note, the blindfold and the read-only text copy -- are all
   `position: absolute; inset: 0` and are unaffected either way. */
.web-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* Why there are two of them: a real browser tab, and this panel's picture of
   it. Dressed as `.web-readonly` because it is the same kind of sentence --
   a standing fact about what you are looking at, said quietly and never
   dismissed. */
.web-live { flex: none; padding: 7px 14px; font-size: var(--fs-2xs-xs); color: var(--text-muted); background: var(--bg-tertiary); border-bottom: 1px solid var(--border); }
.web-live[hidden] { display: none; }
.web-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
.web-mirror { flex: 1; min-height: 0; width: 100%; object-fit: contain; object-position: top; background: #fff; }
.web-url { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); }
.web-note { position: absolute; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 28px; text-align: center; background: var(--bg-secondary); color: var(--text-secondary); font-size: var(--fs-sm); line-height: 1.6; }
.web-note.on { display: flex; }
.web-note b { color: var(--text-primary); }
.web-note button { background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 8px 14px; font-size: var(--fs-sm); }

/* Who is driving, said in the header rather than buried. */
.web-mode { flex: none; font-size: var(--fs-3xs-2xs); letter-spacing: 0.03em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--text-muted); white-space: nowrap; }
.web-mode.agent { color: var(--accent-text); background: var(--accent-soft); border-color: var(--accent); }
.web-mode.user { color: var(--ok); background: var(--ok-bg); border-color: var(--ok); }

/* The blindfold. Not a promise to look away — nothing is sent. The panel says
   so, because a security property the user cannot see is one they cannot
   trust. */
.web-blind { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 32px; text-align: center; background: var(--bg-secondary); }
.web-blind-eye { font-size: var(--fs-6xl); color: var(--ok); opacity: 0.85; }
.web-blind-title { font-size: var(--fs-xl); font-weight: 600; color: var(--text-primary); }
.web-blind-note { font-size: var(--fs-sm); color: var(--text-secondary); line-height: 1.6; max-width: 42ch; }
.web-takeover { margin-top: 6px; background: var(--accent); color: var(--on-fill, #fff); border: none; border-radius: var(--radius-sm); padding: 9px 18px; font-size: var(--fs-base); }
.web-takeover:hover { background: var(--accent-hover); }

/* A stage panel's header reuses the AI panel's chrome.

   And it WRAPS. `.chead-right` is `flex: none` and only `.panel.ai .ctitle`
   carried `min-width: 0`, so in a narrow stage seat these three heads squeezed
   the title to nothing and then pushed the button row out of the panel: at a
   205px seat the head overflowed by 23px and the close button sat at 205..227
   — entirely outside the panel it closes, so the Web panel could not be shut
   from its own header. Measured at three widths; 300 and 250 were still inside.
   A row that drops is a row you can still press. */
.web .chead, .doc .chead, .msg .chead { gap: 8px; flex-wrap: wrap; row-gap: 4px; }
/* AND THE BASIS IS ZERO, NOT `auto`, WHICH IS WHAT MAKES THE ROW ABOVE A LAST
   RESORT RATHER THAN THE USUAL CASE. A wrapping flex container breaks its lines
   on each item's flex BASE size, before any shrinking -- so with `auto` a title
   whose text is 800px wide claimed a line of its own at any panel width, and
   pushed the button row onto a third. Measured 2026-08-28 in a 413px Web panel:
   an ordinary URL took the head from 53px to 102px and left the closer at 229px
   from the panel's right edge and 65px down, which is the fault reported as "the
   cross should be at a fixed position in the top right corner". With a basis of
   zero the title never forces a break, ellipsises as it was always meant to, and
   the row still drops at the 205px seat the comment above measured. */
.web .ctitle, .doc .ctitle, .msg .ctitle { flex: 1 1 0; min-width: 0; }
/* The Doc panel holds the file as characters. The Preview panel holds what it
   looks like: a PDF in `.doc-embed`, anything else the viewer draws in
   `#pv-view`, and the live typeset pages in front of both. Each fills its
   panel; whichever is showing, the others are display:none. */
.doc-view { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
.doc-embed { flex: 1; width: 100%; min-height: 0; border: 0; background: var(--bg-tertiary); }
/* Main needs room to be Main — but only where there is room to give. On a phone
   one panel fills the screen, so a desktop minimum would just force an overflow. */
/* The stage needs room to be the stage — but only where there is room to give.
   On a phone one panel fills the screen, so a desktop minimum would just force
   an overflow. */
@media (min-width: 761px) { #stage { min-width: 420px; } }
@media (max-width: 760px) { #stage { min-width: 0; } }


body.resizing { cursor: col-resize; user-select: none; }

/* The theme control is an icon button like the rest now, not a pair of emoji
   sitting among a hand-drawn SVG set. */
.theme-toggle .ic { width: 18px; height: 18px; }

/* A page the gateway read for us. It is a COPY, not a session on the site — the
   badge says so, because a user who mistakes it for the live page might try to
   sign in to it, and a page served from our origin is a page we can read. */
.web-text { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--bg-secondary); overflow: hidden; }
.web-readonly { flex: none; padding: 7px 14px; font-size: var(--fs-2xs-xs); color: var(--text-muted); background: var(--bg-tertiary); border-bottom: 1px solid var(--border); }
.web-text-body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px 20px; font-size: var(--fs-base); line-height: 1.65; white-space: pre-wrap; word-break: break-word; color: var(--text-primary); }


/* The permission chip: a word in the chat header saying what Daimond does
   without asking. The WORD carries the state — the dot and the accent only
   help the eye land, so nothing is lost on a palette or to a reader who
   cannot tell the colours apart. */
.mode-chip { display: inline-flex; align-items: center; gap: 5px; }
.mode-chip-dot {
	flex: none;
	width: 6px; height: 6px;
	border-radius: 50%;
	background: var(--text-muted);
	/* Measured, not nudged. dev/shot_permchip.mjs reads the font's own metrics and
	   asserts this number; dev/shot_permchip_ladder.mjs is the 8x crop it was
	   chosen from.

	   Two things the line box gets wrong. Its centre sits 1px above the band the
	   letters occupy, because the ascent carries accent space nothing in these
	   labels uses. And the three labels disagree with each other: "Guarded" has an
	   ascender and no descender, "Bypass" has both, and their ink centres are 2px
	   apart — so no single offset centres the dot on all three.

	   This is the midpoint of those two extremes, which is the offset whose WORST
	   case is smallest: 1px out on either, rather than 2px out on one. At 8x, +1px
	   reads low against "Guarded" and -0.5px reads high against "Bypass"; this
	   reads level against all three. */
	transform: translateY(0.5px);
}
.mode-chip[data-mode="ask"]    .mode-chip-dot { background: var(--ok); }
.mode-chip[data-mode="bypass"] .mode-chip-dot { background: var(--accent); }
/* This chat has lost the network, which outranks the rung's own tint on the one
   mark the chip has. No conflict with the bypass accent above: bypass withholds
   nothing, so a bypass chip is never cut. LAST, so it wins on order.

   The dot is the right place for it because the dot was never carrying anything
   -- the rung is the word beside it, and index.html says so: "The word IS the
   state; the accent is only so the eye lands." The colour alone is not the
   signal; `handmode.js` puts "no network" into the chip's aria-label, and the
   popover says it in a sentence. */
/* THE PILL'S OWN EDGE, not only the dot. Tried the dot alone first, twice: amber
   against grey is a 6px difference somebody has to already know about, and an
   open ring inside 6px leaves a 2px hole that reads at real size as a slightly
   lighter dot rather than as a ring. Both were looked at, blown up 8x, and
   rejected there -- the sweep would have passed either.

   The edge is 60px of line instead of 6px of fill, it costs no width on a phone,
   and it does not disturb the ladder's own colours. The dot follows it so the two
   marks are one signal. */
.mode-chip.net-cut { border-color: var(--warn); }
.mode-chip.net-cut .mode-chip-dot { background: var(--warn); }

/* The picker. `.pop` already carries the surface, the shadow and the scroll. */
.mode-pop .mode-row {
	display: flex;
	align-items: flex-start;
	gap: 8px;
	padding: 7px 6px;
	border-radius: var(--radius-sm);
	cursor: pointer;
}
.mode-pop .mode-row:hover { background: var(--bg-hover); }
.mode-pop .mode-row.on { background: var(--accent-soft); }
.mode-pop .mode-row input { margin-top: 3px; flex: none; accent-color: var(--accent); }
.mode-pop .mode-row-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mode-pop .mode-row-name { color: var(--text-primary); font-size: var(--fs-xs); }
.mode-pop .mode-row-blurb { color: var(--text-secondary); font-size: var(--fs-2xs); line-height: 1.4; }
.mode-pop .mode-row:focus-within { outline: 2px solid var(--accent); outline-offset: -2px; }
/* This chat's own network, below the rungs. `.pop-head` already draws the rule
   that separates the two scopes, so the note under it must not draw a second one
   three pixels lower. */
.mode-pop .net-now { border-top: none; margin-top: 0; padding: 2px 6px 0; color: var(--text-secondary); }
/* The three standing choices, one row. Chips and not a second radio ladder: the
   rungs above are a policy with a reason each, this is one answer with three
   values, and giving it the ladder's shape would say the two were the same kind
   of thing. `wrap` because the longest set -- French -- does not fit 380px. */
.mode-pop .net-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 6px 2px; }
.mode-pop .net-opt { flex: none; }

/* ── Triggered actions, in a Diamond's settings dialog ───────────
   One row per action: its light, what it is in words, and the way in.
   Notes2 warns about clutter here by name — "To avoid clutter, Instruction and
   Context should just show an edit button and a copy button" — so the two long
   texts are behind buttons and never inline. Eight actions with their
   instructions on the page would be a dialog nobody reads. */
.trig-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
.trig-row { display: flex; align-items: center; gap: 6px; padding: 4px 0; }
.trig-name { flex: 1 1 auto; min-width: 0; font-size: var(--fs-sm); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Where a control would be, on the one action that has none: `prompted` is the
   daimon answering you, and `/self` is already its control. 59px is the group's
   own width -- 20 for each verb, 13 for the lamp, 3 of gap between them -- so
   the name beside it starts on the same edge as every other row's. */
/* `--border-strong`, not `--border`: a divider separates two things that are
   already there, and a control's edge has to say the control IS there. Phase H
   copied this rule from a box rather than from a button. */
.trig-btn {
	flex: none; width: 26px; height: 26px; border-radius: var(--radius-sm);
	background: var(--bg-tertiary); border: 1px solid var(--border-strong);
	color: var(--text-secondary); font-size: var(--fs-sm); line-height: 1;
}
.trig-btn:hover { border-color: var(--accent); color: var(--text-primary); }
/* The chooser: the light, the pulldown naming every action, and the way to
   remove the one showing. One row, and it does not change shape when a
   different action is chosen -- the light is the control reached for most and
   it should not move under the pointer. */
.trig-pick { display: flex; align-items: center; gap: 6px; padding: 2px 0 6px; }
.trig-choose { flex: 1 1 auto; min-width: 0; }
/* The chosen action's own settings, set in from the chooser so it reads as
   belonging to it rather than as the next thing down the dialog. */
.trig-panel {
	display: flex; flex-direction: column; gap: 8px;
	padding: 8px 0 2px 10px;
	border-left: 2px solid var(--border);
}
/* A long text: what it says, then Edit and Copy. The gist is what stops two
   of these rows being indistinguishable from each other. */
.trig-text-row { display: flex; align-items: center; gap: 6px; }
.trig-text-row .trig-label { flex: none; }
.trig-gist {
	flex: 1 1 auto; min-width: 0;
	font-size: var(--fs-sm); color: var(--text-secondary);
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trig-gist.empty { color: var(--text-muted); font-style: italic; }
.trig-add { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
.trig-add .tile-model { flex: 1 1 auto; min-width: 0; }
.trig-edit { display: flex; flex-direction: column; gap: 10px; }
.trig-field { display: flex; flex-direction: column; gap: 4px; }
.trig-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.trig-input, .trig-area {
	background: var(--bg-tertiary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); color: var(--text-primary);
	padding: 6px 8px; font-size: var(--fs-sm); width: 100%;
}
.trig-area { font-family: var(--font-mono); resize: vertical; min-height: 64px; }
.trig-input:focus, .trig-area:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

/* ── Pending ─────────────────────────────────────────────────────
   A card per thing waiting on you. The headline is a button because it
   EXPANDS: a panel of paragraphs cannot be scanned and a panel of one-liners
   cannot be judged, so it is both and the reader chooses which. */
.pending-actions { display: flex; align-items: center; gap: 6px; }
.pending-sort { background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-secondary); font-size: var(--fs-2xs); padding: 2px 4px; max-width: 120px; }
.pending-list { display: flex; flex-direction: column; gap: 6px; padding: 8px; overflow-y: auto; }
.pend-card {
	background: var(--bg-tertiary); border: 1px solid var(--border);
	border-left-width: 3px; border-radius: var(--radius-sm); padding: 8px 10px;
	display: flex; flex-direction: column; gap: 6px;
}
/* Priority as an EDGE, not a fill: the card stays readable and the column of
   edges down the left is what makes a list scannable at a glance. */
.pend-card.prio-high   { border-left-color: var(--danger); }
.pend-card.prio-normal { border-left-color: var(--accent); }
.pend-card.prio-low    { border-left-color: var(--border-strong); }
.pend-head { display: flex; align-items: center; gap: 6px; font-size: var(--fs-2xs); color: var(--text-muted); }
.pend-prio { background: transparent; border: 1px solid var(--border); border-radius: 20px; color: var(--text-muted); font-size: var(--fs-3xs); padding: 0 4px; }
.pend-diamond { font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pend-when { margin-left: auto; flex: none; }
.pend-line {
	background: none; border: 0; padding: 0; text-align: left;
	color: var(--text-primary); font-size: var(--fs-sm); line-height: 1.4;
}
.pend-line:hover { color: var(--accent-text); }
.pend-line:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pend-detail { font-size: var(--fs-xs); color: var(--text-secondary); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
.pend-files { display: flex; flex-wrap: wrap; gap: 4px; }
.pend-file { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 20px; color: var(--text-muted); font-size: var(--fs-3xs); padding: 1px 8px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pend-file:hover { color: var(--text-primary); border-color: var(--accent); }
.pend-acts { display: flex; gap: 6px; }
.pend-act { flex: 1; border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--bg-secondary); color: var(--text-secondary); padding: 4px 0; font-size: var(--fs-sm); }
.pend-act:hover { color: var(--text-primary); }
.pend-go:hover  { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-text); }
.pend-no:hover  { border-color: var(--danger); color: var(--danger); }
.pend-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── The `/` menu ────────────────────────────────────────────────
   Skills have worked since seq 65 and nothing told anyone they existed. A `/`
   alone in an empty composer lists them. Above the box, because the composer
   sits at the foot of the panel and a menu below it would be off-screen. */
.skill-menu {
	position: fixed; z-index: 60; max-height: 40vh; overflow-y: auto;
	background: var(--bg-secondary); border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm); box-shadow: 0 8px 24px rgba(0,0,0,0.28);
	display: flex; flex-direction: column; padding: 4px;
}
.skill-item {
	background: none; border: 0; text-align: left; padding: 6px 10px;
	border-radius: var(--radius-sm); color: var(--text-primary);
	font-family: var(--font-mono); font-size: var(--fs-sm);
}
.skill-item:hover { background: var(--accent-soft); color: var(--accent-text); }
.skill-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.skill-none { padding: 8px 10px; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }
/* An empty Diamond chat says what it is, rather than looking like a thread that
   failed to load. Centred and quiet: it is a signpost, not a message. */
.chat-msg-empty {
	color: var(--text-muted); font-size: var(--fs-sm); line-height: 1.6;
	text-align: center; max-width: 34rem; margin: 12vh auto 0; padding: 0 16px;
}

/* ── The trail on the lock screen ────────────────────────────────────────────
   Only on screen when the app has booted three times in ninety seconds, which
   is not something a person does. It exists because a loop on an iPhone could
   be described but never seen: reading a browser console on iOS needs a Mac
   attached, so three reports produced two wrong diagnoses and no evidence. */
.id-trail {
	margin-top: 1rem;
	padding-top: 0.75rem;
	border-top: 1px solid var(--border);
	text-align: left;
}
.id-trail-lead {
	margin: 0 0 0.5rem;
	font-size: var(--fs-xs);
	color: var(--danger);
}
/* Monospace and scrollable: it is a log, and a log that wraps into prose
   cannot be read at a glance on a phone. */
.id-trail-text {
	margin: 0;
	max-height: 34vh;
	overflow: auto;
	padding: 0.5rem 0.6rem;
	background: var(--bg-tertiary);
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	font-family: var(--font-mono);
	font-size: var(--fs-3xs-2xs);
	line-height: 1.5;
	color: var(--text-secondary);
	white-space: pre;
	-webkit-overflow-scrolling: touch;
}
.id-trail-acts { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.id-trail-btn {
	flex: 1;
	padding: 0.4rem 0.6rem;
	font-size: var(--fs-xs);
	color: var(--text-secondary);
	background: var(--bg-tertiary);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-sm);
}
.id-trail-btn:hover { color: var(--text-primary); border-color: var(--accent); }
.id-trail-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── An answer folded in two ──────────────────────────────────────────
   `say` returns a summary and a detail. The summary reads as an ordinary
   answer, because that is what it is; the detail sits behind a control that
   says how big it is, since the size is the whole of what a reader needs to
   decide whether to open it.

   The left edge is what marks it as folded rather than merely short. Without
   it, a two-sentence answer with a small control under it looks like an answer
   that got cut off. */
.chat-msg.said { border-left: 2px solid var(--accent-soft); padding-left: 10px; }
.said-more {
	display: block;
	margin: 8px 0 0;
	padding: 4px 0;
	background: none;
	border: none;
	color: var(--text-muted);
	font-size: var(--fs-sm);
	text-align: left;
	cursor: pointer;
}
.said-more:hover { color: var(--accent-text); }
/* Set apart from the summary above it, or an opened fold reads as one long
   answer and the reader loses which part they chose to see. */
.said-detail {
	margin-top: 8px;
	padding-top: 8px;
	border-top: 1px solid var(--border);
}

/* ── A question the model put, answered with one tap ──────────────────
   IT MUST NOT LOOK LIKE A PERMISSION PROMPT. `.modal.dlg` is what asks
   "may I do this": centred, over a scrim, danger-coloured, and it takes
   the screen. This is the opposite question -- "which of these do you
   want" -- so it sits IN the thread, in the accent, takes no focus and
   blocks nothing. The two mean opposite things and a reader has to be
   able to tell them apart at a glance. */
.ask-card {
	margin: 12px 0;
	padding: 12px 14px;
	border: 1px solid var(--accent);
	border-left-width: 3px;
	border-radius: var(--radius);
	background: var(--bg-secondary);
}
.ask-card.answered { border-color: var(--border-2); background: var(--bg-primary); }
.ask-count {
	font-size: var(--fs-2xs);
	color: var(--text-muted);
	text-transform: uppercase;
	letter-spacing: 0.06em;
	margin-bottom: 4px;
}
.ask-q {
	font-size: var(--fs-lg);
	color: var(--text-primary);
	line-height: 1.4;
	margin-bottom: 10px;
}
.ask-opts { display: flex; flex-direction: column; gap: 6px; }
/* One tap answers, so the whole option is the target: the label, the badge
   and what it means are all inside the button. A row where only a small
   word was clickable would be a decision answered by aiming. */
.ask-opt {
	display: block;
	width: 100%;
	text-align: left;
	padding: 8px 10px;
	border: 1px solid var(--border-2);
	border-radius: var(--radius-sm);
	background: var(--bg-tertiary);
	color: var(--text-primary);
	font: inherit;
	cursor: pointer;
}
.ask-opt:hover  { background: var(--bg-hover); border-color: var(--border-strong); }
.ask-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ask-opt.recommended { border-color: var(--accent); background: var(--accent-soft); }
.ask-label { font-size: var(--fs-base); font-weight: 600; }
/* NAMED, not merely first. A recommendation implied by ordering is not one. */
.ask-rec {
	margin-left: 8px;
	padding: 1px 6px;
	border-radius: 999px;
	background: var(--accent);
	color: var(--on-fill);
	font-size: var(--fs-3xs);
	text-transform: uppercase;
	letter-spacing: 0.06em;
	vertical-align: middle;
}
.ask-means {
	display: block;
	margin-top: 3px;
	font-size: var(--fs-sm);
	color: var(--text-secondary);
	line-height: 1.45;
}
.ask-why, .ask-silent {
	margin-top: 8px;
	font-size: var(--fs-sm);
	color: var(--text-secondary);
	line-height: 1.45;
}
.ask-silent { color: var(--text-muted); }
.ask-other { margin-top: 10px; }
.ask-other-open {
	padding: 0;
	background: none;
	border: none;
	color: var(--text-muted);
	font: inherit;
	font-size: var(--fs-sm);
	cursor: pointer;
}
.ask-other-open:hover { color: var(--accent-text); }
.ask-other-row { display: flex; gap: 6px; }
.ask-other-box {
	flex: 1;
	min-width: 0;
	padding: 6px 8px;
	border: 1px solid var(--border-2);
	border-radius: var(--radius-sm);
	background: var(--bg-primary);
	color: var(--text-primary);
	font: inherit;
	font-size: var(--fs-sm);
}
.ask-other-go {
	padding: 6px 10px;
	border: 1px solid var(--accent);
	border-radius: var(--radius-sm);
	background: var(--accent);
	color: var(--on-fill);
	cursor: pointer;
}
/* An answered card keeps its options and stops offering them. What it was
   decided AGAINST is the part somebody comes back to a month later. */
.ask-opt:disabled { cursor: default; opacity: 0.55; }
.ask-opt:disabled:hover { background: var(--bg-tertiary); border-color: var(--border-2); }
.ask-opt.chosen {
	opacity: 1;
	border-color: var(--ok);
	background: var(--ok-bg);
}
.ask-opt.chosen:disabled:hover { background: var(--ok-bg); border-color: var(--ok); }
/* The recommendation badge on a card already answered is history, not advice. */
.ask-card.answered .ask-rec { background: var(--border-2); color: var(--text-secondary); }
.ask-done {
	margin-top: 8px;
	font-size: var(--fs-sm);
	color: var(--ok);
}

/* ── The Wire: the payload, annotated where it lies ───────────────────
   Not a panel beside the thread. The thread IS the accumulated payload,
   laid out once, so this turns on the parts of it that are normally
   invisible rather than printing a second copy of the parts that are not. */
.wire-head {
	margin: 0 0 14px;
	padding: 10px 12px;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--bg-secondary);
}
.wire-title {
	font-size: var(--fs-sm);
	color: var(--text-muted);
	margin-bottom: 8px;
}
.wire-band-head {
	display: flex;
	align-items: baseline;
	gap: 8px;
	width: 100%;
	padding: 5px 0;
	background: none;
	border: none;
	border-top: 1px solid var(--border);
	color: var(--text);
	font-size: var(--fs-sm);
	text-align: left;
	cursor: pointer;
}
.wire-band:first-of-type .wire-band-head { border-top: none; }
.wire-band-name { flex: none; }
/* WHOSE it is, which is the thing this view exists to say. Muted, because it
   is a gloss on the name rather than a second name. */
.wire-band-why  { flex: 1 1 auto; color: var(--text-muted); font-size: var(--fs-xs); }
.wire-band-tok  { flex: none; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.wire-band-body {
	margin: 6px 0 8px;
	padding: 8px 10px;
	max-height: 24rem;
	overflow: auto;
	background: var(--bg-tertiary);
	border-radius: var(--radius-sm);
	font-size: var(--fs-xs);
	white-space: pre-wrap;
	word-break: break-word;
}

/* ── A chip that is a picture ─────────────────────────────────────────
   The row was six words in a line — Guarded, Concise, Wire, Copy, Steps —
   which reads as a sentence you have to parse before you can press
   anything. The verbs become drawings and keep their words on hover; the
   MODE chip stays a word, because it names a state with three values and a
   picture of it would be a guess. */
.chip-btn.icon-chip {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* A FLOOR ON BOTH AXES. Width alone left the height to the row, and at 761-768px it
	   collapsed under 24px — which `verify_sweep_mobile` reported as a target too small to
	   press, on three of these, at exactly the widths a tablet uses. A word-shaped chip had
	   its padding to keep it open; a drawing has nothing but this. */
	/* THE ROW'S OWN HEIGHT, measured off its neighbours rather than chosen: the mode chip
	   and the collapse button are both 26px, and at 30 these sat four pixels taller and
	   square where the rest of the row is a pill. On screen they read as heavier than the
	   words beside them, which is the opposite of what a drawing is here for. */
	width: 26px;
	min-width: 26px;
	height: 26px;
	min-height: 26px;
	padding: 0;
}
.chip-btn.icon-chip .ic { width: 14px; height: 14px; }

/* A WORD AMONG THE DRAWINGS, AT THE DRAWINGS' HEIGHT. One chip in this row keeps
   its label -- "Wire", which is not a picture of anything -- and a `.chip-btn`'s
   own padding would draw it two pixels taller than the 26px boxes either side of
   it, so the row would grow rather than the chip fitting. The height is taken
   from `.icon-chip` above, deliberately, so the two cannot drift apart. */
.chip-btn.label-chip {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	height: 26px;
	min-height: 26px;
	padding: 0 9px;
	line-height: 1;
}
/* Pressed reads as pressed. Without this an icon toggle has no state at all,
   where a word one at least changed weight. */
.chip-btn.icon-chip[aria-pressed="true"] {
	background: var(--accent-soft);
	color: var(--accent-text);
	border-color: var(--accent);
}

/* ── A fold the model wrote itself ────────────────────────────────────
   `<details>` now survives the renderer's whitelist, so a model can fold
   part of an answer away in the answer itself, with no tool involved. The
   class is stamped by the sanitiser (`render.js`) rather than matched on a
   bare `details`, because the app draws one of its own -- the release notes'
   list of sealed builds, `.rel-builds` -- and that one keeps the browser's
   look. A model's fold cannot reach this selector without going through the
   sanitiser first.

   Drawn as the `say` fold above is drawn, and for the same reasons: a left
   edge that says folded rather than merely short, and a rule between the label
   and what it opens onto. The disclosure triangle is the browser's own,
   inheriting `color` from the summary, since a model's summary text can be
   anything and a fold with no affordance but a colour change is a fold nobody
   opens.

   THE SUMMARY IS NOT A CAPTION AND IS NO LONGER DRAWN AS ONE. It was muted at
   `--fs-sm` while the prompt modelled it as "a few words naming what is
   inside", and the owner, 2026-08-23, found a real one on his own screen only
   after being told where to look: four muted words at 13px, between a reply
   above and a reply below, is a caption for something rather than a thing to
   read. `FOLD_NOTE` now asks for a sentence or two of what the fold concludes,
   so the summary carries the substance of the half that is hidden and has to
   be legible at the weight of the answer it belongs to. The left edge and the
   triangle are what say it is foldED; the type no longer says it is minor.

   The thinking tile below KEEPS the quiet treatment, and the difference is the
   reader: that one is the model's own working, not addressed to anybody, and
   it must not compete with the reply. It carries BOTH classes -- `md-fold
   chat-msg-thinking`, `daimond.js:6782` -- so it inherited this rule and had to
   be given the muted size and colour back by name. A comment here saying it was
   untouched would have been true of the intent and wrong about the screen. */
details.md-fold {
	margin: 8px 0;
	padding-left: 10px;
	border-left: 2px solid var(--accent-soft);
}
details.md-fold > summary {
	padding: 4px 0;
	color: var(--text-primary);
	font-size: var(--fs-base);
	cursor: pointer;
}
details.md-fold > summary:hover { color: var(--accent-text); }
details.md-fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Set apart from the label above it, or an opened fold reads as one long
   answer and the reader loses which part they chose to see. Only when open:
   a closed fold is a single line and a rule under it marks off nothing. */
details.md-fold[open] > summary {
	margin-bottom: 8px;
	padding-bottom: 8px;
	border-bottom: 1px solid var(--border);
}
/* The last paragraph inside carries the bottom margin every `p` gets, which
   below a border reads as the fold not having ended. */
details.md-fold > :last-child { margin-bottom: 0; }

/* The model's own working. Same disclosure as a two-depth answer, quieter: it
   is not addressed to the reader, so it must not compete with the reply above
   it. Pre-wrap because reasoning arrives with its own line breaks and nothing
   parses it into paragraphs. */
details.chat-msg-thinking > summary {
	font-style: italic;
	opacity: 0.72;
	/* Taking back what `details.md-fold > summary` now gives every fold. Same
	   specificity, later in the file, so these win. */
	color: var(--text-muted);
	font-size: var(--fs-sm);
}
.chat-thinking-body {
	white-space: pre-wrap;
	font-size: 0.92em;
	opacity: 0.86;
}

/* ── The same fold, drawn while it is still arriving ──────────────────
   A fold that streams keeps ONE `<details>` for the whole of a generation --
   rebuilt per frame it would snap shut sixty times a second -- so the chat
   redraws its body inside a wrapper rather than the element itself, and the
   runs of ordinary answer around it get wrappers of their own. Neither is a
   box: no margin, no padding, no rule, so a fold watched arriving and the same
   fold after a reload are the same picture.

   `> :last-child` above lands on the wrapper rather than on the paragraph it
   holds, which is why the rule is repeated one level in. */
details.md-fold > .md-fold-body > :last-child { margin-bottom: 0; }
.chat-msg-content > .md-seg > :last-child { margin-bottom: 0; }
