Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/css/app.css

235 KiB, 8 runs

created by r2519314175:1091, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2html, body { height: 100%; width: 100%; overflow: hidden; }
3body { 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; }
4/* A palette may lay a wash over the frame -- Lollypop's multicolour glow is one
5 -- by declaring --body-wash. It is fixed, so it sits still behind the panels
6 as they scroll, and the panels themselves are opaque, so a wash never touches
7 the legibility of the words on top. Declared as a token rather than as a rule
8 naming one palette, because a rule naming a palette is a rule every palette
9 added afterwards silently misses. */
10body { background-image: var(--body-wash, none); background-attachment: fixed; }
11button, input, textarea, select { font-family: inherit; font-size: inherit; color: inherit; }
12button { cursor: pointer; }
13svg { display: block; }
14.ic { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.75; fill: none; stroke-linecap: round; stroke-linejoin: round; flex: none; }
15
16/* ── The small plates ────────────────────────────────────────────
17 The rail heads' `+`, the graph latch, the panel closers, the agent brakes and
18 a triggered action's way in are all the same idea: one glyph on a small
19 square. They wore a rotated diamond for one release, borrowed from the
20 splash page's mark. It came back off because a diamond inscribed in a box
21 has HALF the box's area, so the box had to grow from 22px to 30px to keep
22 the glyph off the edges -- and 8px on every one of these, in a rail head and
23 a top bar, is real estate this app has none of to give. The mark stays on
24 the splash page, where there is room for it.
25
26 The shape and paint are each button's own, below; what stays shared is the
27 focus ring, because it must be a real `outline` ON THE BUTTON.
28 `verify_a11y_keyboard` proves a control is focusable by snapshotting the
29 element's own outline and shadow and requiring them to change, and its
30 self-test strips `.addbtn`'s outline BY NAME to prove the census works. A
31 ring drawn on a pseudo-element would be a control that census calls
32 ringless, and a ring made of a background or border change would leave that
33 self-test unable to go red. So: an outline, on the button, and no
34 `border-radius` of its own -- an outline follows the corner the button is
35 already cut with, which is the square's. */
36.addbtn:focus-visible, .agc:focus-visible, .trig-btn:focus-visible {
37 outline: 2px solid var(--accent);
38 outline-offset: 1px;
39}
40
41/* ── Copy an identifier ──────────────────────────────────────────
42 One button, wherever a machine-readable string is on show: the build id in
43 the rail, the account fingerprint, a device id, a build in the version
44 history. It is deliberately quiet -- these sit beside values, not on top of
45 them -- and comes up to full strength on hover, on focus, and whenever the
46 pointer is anywhere on the line it belongs to.
47 `align-self: center` because several of its hosts are baseline-aligned rows:
48 a 20px box hung by a baseline overshoots the line and takes the row's height
49 with it, which is the same fault `.device-rename` had.
50 The 20px box sits UNDER the 24px target floor, and does so knowingly: these
51 are secondary controls beside a value, the touch surfaces of the app are
52 sized in mobile.css, and a 24px square in the 220px rail crowds the very
53 string it is there to serve.
54 The tick swaps in place on the same 24-unit grid, so a confirmation never
55 moves what is beside it. */
56.copy-id {
57 flex: none; align-self: center;
58 display: inline-flex; align-items: center; justify-content: center;
59 width: 20px; height: 20px; padding: 0; margin-left: 2px;
60 border: 0; border-radius: 5px; background: transparent;
61 color: var(--text-muted); line-height: 0; cursor: pointer;
62 opacity: .6; transition: opacity 0.12s, color 0.12s;
63 /* For the hosts that are not flex rows -- the Models panel's footer is a
64 block of text -- so the box rides the line rather than hanging off its
65 baseline and stretching it. Ignored inside a flex container. */
66 vertical-align: middle;
67}
68.copy-id .ic { width: 13px; height: 13px; }
69.copy-id:hover, .copy-id:focus-visible { opacity: 1; color: var(--text-primary); background: var(--bg-hover); }
70/* The row a copy button sits on brings it up as the pointer arrives, so a
71 column of them does not compete with the values they belong to. */
72.astat-copyrow:hover .copy-id,
73.device-row:hover .copy-id,
74.account-fp:hover .copy-id,
75.models-default:hover .copy-id,
76.rel-row:hover .copy-id,
77.rel-build:hover .copy-id { opacity: 1; }
78/* Done, and not done. The colour is the confirmation; nothing moves. */
79.copy-id.copied { opacity: 1; color: var(--ok); }
80.copy-id.failed { opacity: 1; color: var(--danger); }
81.copy-id:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
82
83/* Login */
84.login-screen { display: flex; align-items: center; justify-content: center; height: 100vh; background: var(--bg-primary); }
85.login-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 32px; width: 360px; max-width: 90vw; }
86.login-logo { height: 64px; margin: 0 auto 20px; display: block; }
87.login-card form { display: flex; flex-direction: column; gap: 12px; }
88.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); }
89.login-card input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
90.login-card input::placeholder { color: var(--text-muted); }
91.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); }
92.login-card button:hover { background: var(--accent-hover); }
93.login-error { color: var(--danger); font-size: var(--fs-sm); margin-top: 8px; min-height: 18px; }
94.login-card a { display: block; text-align: center; margin-top: 16px; color: var(--accent-text); text-decoration: none; font-size: var(--fs-base); }
95
96/* Said to a screen reader and to nothing else. The app's own name is drawn as a
97 wordmark, which is an image and cannot be a heading, so the document had no
98 <h1> at all and heading navigation started at level 2 with no level 1 above it.
99 Clipped rather than `display: none`, which would take it out of the tree along
100 with the pixels. */
101.vh {
102 position: absolute;
103 width: 1px; height: 1px;
104 margin: -1px; padding: 0; border: 0;
105 overflow: hidden;
106 clip: rect(0 0 0 0);
107 clip-path: inset(50%);
108 white-space: nowrap;
109}
110
111/* ── App frame: topbar + four panels ─────────────────────────── */
112/* The safe-area insets are added to the frame's own padding, not substituted for
113 it: installed on a home screen there is no browser chrome holding the notch and
114 the home indicator off the layout, and the top bar landed under the status bar.
115 They are all zero in a browser tab, so this is the same padding it always was. */
116#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; }
117
118.topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 4px; }
119.brand { display: flex; align-items: center; text-decoration: none; transition: opacity 0.15s ease; }
120.brand:hover { opacity: 0.82; }
121/* The integrated wordmark (mark + word as one vector), the same asset the unlock
122 screen uses, in place of the mark plus a UI-font "Daimond". Two variants swap
123 by theme because the topbar sits on the theme background: white lettering on
124 the dark theme, dark grey on the light and lollypop themes. */
125.brand-wordmark { height: calc(var(--logo-h) * 1.5); width: auto; display: block; }
126/* The brand group. The wordmark was one of two marks here; the maker's badge has
127 since moved to the other end of the bar, so this is now a group of one -- kept
128 as a group because the bar distributes its children with space-between, and a
129 wordmark that is a direct child of that gets a different left edge. */
130.brand-group { display: flex; align-items: center; flex: none; min-width: 0; }
131/* The maker's badge no longer lives in this row -- it is in the About dialog,
132 which the button in its old slot opens. Its rules are down with the dialog's,
133 because that is the only place it is drawn now. */
134.brand-wordmark.wm-on-light { display: none; }
135:root[data-ink="dark"] .brand-wordmark.wm-on-dark { display: none; }
136:root[data-ink="dark"] .brand-wordmark.wm-on-light { display: block; }
137.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; }
138.top-meter .sep { color: var(--text-muted); }
139/* NOTHING IN THIS GROUP MAY MOVE ITS NEIGHBOURS. The bar distributes its three
140 children with space-between, so while this box shrank to its contents it sat
141 flush right and every pixel that appeared inside it pushed the chip row left:
142 measured at 1440px, a "Syncing..." pill arriving moved all thirteen chips
143 86px, the pairing button another 122, and a held-back sweep 98. That is a row
144 of targets that jumps while a thumb is on its way to one.
145 `flex: 1 1 auto` here, and on `.panel-tags` below, fixes both ends instead:
146 the row starts where the wordmark leaves off and the icons end at the bar's
147 own edge, whatever is or is not between them. `justify-content: flex-end`
148 is for the phone, where the chip row is not in this box at all (it is the
149 footer there) and the icons would otherwise pack to the left. */
150.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; min-width: 0; flex: 1 1 auto; }
151
152.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); }
153.icon-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
154.icon-btn.on { color: var(--accent); }
155.icon-btn.dim { opacity: 0.4; }
156/* One icon size across the top bar. The update chip and the appearance menu
157 already drew at 18px while the guide "?" and the pairing glyph took the
158 .icon-btn default of 16, so the row of five read as two different sets. */
159.top-actions .icon-btn .ic { width: 18px; height: 18px; }
160
161
162.main { display: flex; flex: 1; min-height: 0; }
163
164/* Panels. The rail carries an inline width the user can pull; a dock panel
165 fills its column, and the stage's occupants are sized by the engine. */
166/* Scrollbars, everywhere. The browser's own paints a stepper arrow at each end
167 * and a track in a colour of its choosing, which lands as a pale strip down the
168 * side of a dark panel. Keep the width — it is comfortable to grab — and drop
169 * the rest: no arrows, and a track that is simply the surface behind it, so the
170 * only thing drawn is the thumb. */
171::-webkit-scrollbar { width: 14px; height: 14px; }
172::-webkit-scrollbar-track { background: transparent; }
173::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
174::-webkit-scrollbar-corner { background: transparent; }
175::-webkit-scrollbar-thumb {
176 background: var(--border-2);
177 border-radius: 999px;
178 border: 4px solid transparent; /* Inset, so the thumb floats rather than fills. */
179 background-clip: content-box;
180}
181::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: content-box; }
182
183/* Firefox has no ::-webkit-scrollbar, so it is given the standard properties
184 * instead. They are withheld from engines that do have it: Chromium honours
185 * them, but the moment it does it stops honouring the rules above — including
186 * the one that removes the arrows — and paints its own stepper buttons back. */
187@supports not selector(::-webkit-scrollbar) {
188 * { scrollbar-width: auto; scrollbar-color: var(--border-2) transparent; }
189}
190
191.panel { min-height: 0; }
192/* Spending belongs in this list with its dock siblings. Left out, it stayed a
193 block -- which reads correctly only for as long as nothing sets `display:
194 flex` on it. The phone sheet does exactly that (`#msheet .panel`), and the
195 panel's head and its body then laid out side by SIDE: a stranded "SPENDING"
196 column on the left and the figures squeezed into what was left. */
197/* Social belongs here for the same reason, and was left out in the same way.
198 As a block it made `.imp-view { flex: 1 }` and `.imp-notes { flex: 1;
199 overflow-y: auto }` (css/improve.css) both inert: a flex child of nothing is
200 a block that takes its content's height, so `overflow-y: auto` never had an
201 overflow to engage on. Measured 2026-08-28 with twenty notes kept: the panel
202 was clipped at 434px and the list ran to 2606px, so NOT ONE note was on the
203 screen and nothing would scroll to them. */
204.rail, .agents, .work, .mail, .spend, .social { flex: none; display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
205/* The `gap: 6px` above falls between the head and the body and NOWHERE ELSE
206 here, which is why this panel needs no correction for it: every other child
207 of `.social` is an `.imp-view`, and `.imp-view[hidden]` is `display: none`,
208 so exactly one of the five is ever in the flow. Measured after the change:
209 the note box starts 6px lower than it did and nothing else moved. */
210/* Now that the panel is a flex column, the view is its flexible child: its own
211 `height: 100%` would otherwise sit BESIDE the head in the height budget and
212 push its own last rows out under the clip. */
213.spend > .spend-view { flex: 1 1 auto; min-height: 0; }
214.rail { width: var(--rail-w); gap: 0; }
215.panel.nt { transition: none; }
216.panel.closed { display: none; }
217
218/* Every stage panel is the same card: the AI, a page, a document, a message.
219 NOT the whole list -- the Graph and the Terminal wear the same card, declared
220 in the file each of them owns (graph.css, terminal.css) rather than here. A
221 stage panel that appears on none of the three lists is a bare block on the
222 stage's ground, and worse than bare: a block is not a flex column, so nothing
223 inside it can be told to fill the height. */
224.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; }
225
226/* Drag handles between panels. */
227.phandle { width: 10px; flex: none; cursor: col-resize; position: relative; touch-action: none; }
228.phandle::after { content: ""; position: absolute; top: 0; bottom: 0; left: 4px; width: 2px; border-radius: 2px; background: transparent; transition: background 0.15s; }
229.phandle:hover::after, .phandle.dragging::after { background: var(--accent); }
230.phandle.closed { display: none; }
231body.dragging { cursor: col-resize; user-select: none; -webkit-user-select: none; }
232
233/* Rail/agents/work section headers. */
234.railhead { display: flex; align-items: center; gap: 6px; padding: 2px 4px; }
235/* The Spending header wraps its controls in a span; without this they lay out
236 as inline text rather than as the button row every other header has. */
237.spend-actions { display: flex; align-items: center; gap: 6px; }
238.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; }
239.rail-count { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); white-space: nowrap; }
240
241.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; }
242.addbtn:hover { color: var(--text-primary); }
243.addbtn:disabled { opacity: 0.4; cursor: default; }
244.addbtn:disabled:hover { border-color: var(--border); color: var(--text-secondary); }
245/* A rail-head button that LATCHES has to look latched. `#link-graph-btn` opens
246 the Graph and closes it again, so it carries `aria-pressed` -- and a state a
247 screen reader announces while the screen shows nothing is half a control:
248 press it, the panel goes away, and the button looks exactly as it did. Same
249 treatment as `.tile-dlg-level[aria-pressed="true"]`, the app's other latched
250 button, so the two read as one idea. */
251.addbtn[aria-pressed="true"] {
252 background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text);
253}
254label.addbtn { cursor: pointer; }
255.rail-note { font-size: var(--fs-xs); color: var(--text-muted); padding: 0 6px 4px; }
256
257/* ── The PPTW: play, pause, traffic light ─────────────────────────
258 Drawn identically wherever it appears -- at the head of the rail for the
259 root of the tree, on each Diamond tile, on each chat tile, and (phase G and
260 H) on each mailbox, folder and triggered action.
261
262 The name is the specification (notes2.txt line 3): "Pause/Play/Traffic light
263 widgets (PPTWs) for all spendable functions. Green = all spendable functions
264 active, Amber = some are active, Red = all paused." Play, pause, then the
265 light, on the right.
266
267 THE LIGHT CARRIES NOTHING BUT A COLOUR. It is a lamp, not an icon: no glyph,
268 no symbol, no shape inside it. A glyph in the light is a verb worn as a noun,
269 which is the fault the compact single button had -- a running node drew a
270 green PLAY triangle, so the shape said "play" while the only thing pressing
271 it could do was pause. Drawing that glyph inside the lamp puts the same
272 confusion one place to the right.
273
274 The light is not a button and takes no hover, no focus ring and no pointer
275 cursor. Amber is derived and can never be set -- with nothing to press, that
276 is true by construction rather than by convention. */
277.pptw {
278 flex: none; display: inline-flex; align-items: center; gap: 3px;
279 line-height: 0;
280 /* NOT the tile's pointer, which is inherited. A tile says "press me and I
281 open"; this strip does not open anything, and the light is not pressable
282 at all. It also keeps the light out of the keyboard census: an element
283 drawn as clickable that no key can reach is a defect by definition, and
284 the light's whole point is that it is not clickable. */
285 cursor: default;
286}
287/* 13px, not the buttons' 20: a lamp beside two glyphs has to read as a
288 different KIND of thing, and size is what says so when there is no shape
289 inside it to say it. A ring at 12% of the colour lifts it off a tile whose
290 ground happens to sit near the lamp's own hue. */
291.pptw-lamp {
292 flex: none; width: 13px; height: 13px; border-radius: 50%;
293 background: var(--text-muted);
294 box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-muted) 12%, transparent);
295}
296.pptw[data-state="play"] .pptw-lamp {
297 background: var(--ok);
298 box-shadow: 0 0 0 1px color-mix(in srgb, var(--ok) 12%, transparent);
299}
300.pptw[data-state="pause"] .pptw-lamp {
301 background: var(--danger);
302 box-shadow: 0 0 0 1px color-mix(in srgb, var(--danger) 12%, transparent);
303}
304.pptw[data-state="mixed"] .pptw-lamp {
305 background: var(--warn);
306 box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 12%, transparent);
307}
308
309/* The two verbs. Both are always present and the inapplicable one is DISABLED
310 rather than hidden, so a row never reflows under the thumb and pause is
311 always in the same place. On an amber branch both are live -- which is the
312 case one button had to guess at. */
313.pptw-act {
314 flex: none; width: 20px; height: 20px; padding: 0;
315 display: flex; align-items: center; justify-content: center;
316 border: none; border-radius: var(--radius-sm); background: none;
317 line-height: 0; color: var(--text-secondary); cursor: pointer;
318}
319.pptw-act:disabled { cursor: default; }
320.pptw-act:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-hover); }
321.pptw-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
322/* Dimmed, not gone. A disabled verb still says what the control can do; it is
323 the state that says why it cannot be done now. */
324.pptw-act:disabled { color: var(--text-muted); opacity: 0.32; }
325
326/* Centred by the flex box above and by the viewBox: the ink sits at 8,8 of
327 0 0 16 16, so it is on the box's centre and not merely the sprite. */
328.pptw-act .pptw-ic { width: 12px; height: 12px; display: block; fill: currentColor; }
329
330/* The global control's own row, above the Diamonds list.
331 Deliberately NOT a .railhead and deliberately not dressed as one. Given the
332 section-header look, "EVERYTHING" sat a light's width to the right of
333 "DIAMONDS" and "CHATS" and read as a third section that had lost its
334 alignment. It is not a section, it is a switch: sentence case, a rule under
335 it, and the light leading the way it leads on every tile below. The row's
336 leading ink starts on the same 4px as the section labels, so the two kinds
337 of row share a left edge without pretending to be the same kind.
338
339 It also carries the rail's closer, at the far end. The cross used to sit on
340 the Diamonds head below, where a cross at the end of a row named "Diamonds"
341 reads as the closer of that section and was in fact the closer of the whole
342 rail. This row is the one that speaks for all of it, so the closer and the
343 pause now have the same scope and sit on the same line. The 4px right padding
344 is the same 4px the section headers use, so the cross keeps a right edge with
345 the buttons on the row below. */
346.pptw-head {
347 display: flex; align-items: center; gap: 6px;
348 padding: 4px 4px 6px; border-bottom: 1px solid var(--border);
349}
350.pptw-slot { display: flex; flex: none; }
351/* Ellipsised rather than nowrap alone: `flex: 1` gives it a zero basis but an
352 AUTO minimum, so a longer translation of "Everything" would refuse to shrink
353 and shove the closer off the end of the row. */
354.pptw-head-label {
355 flex: 1; min-width: 0; font-size: var(--fs-sm); font-weight: 500;
356 color: var(--text-secondary); white-space: nowrap;
357 overflow: hidden; text-overflow: ellipsis;
358}
359/* The closer holds its 22px against a long label. */
360.pptw-head .panel-close { flex: none; }
361
362.user-info { flex: 1; font-size: var(--fs-sm); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 4px; }
363#logout-btn:hover { color: var(--danger); }
364
365/* ── The rail: work above, the Admin panel below ─────────────────
366 The divider moves; it does not go away. */
367.rail-top { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; min-height: 0; overflow: hidden; }
368.admin { display: flex; flex-direction: column; gap: 6px; flex: none; position: relative; }
369
370/* The horizontal twin of .phandle. Its line is the divider, so nothing below
371 needs a border of its own. */
372.rhandle { height: 10px; flex: none; cursor: row-resize; position: relative; touch-action: none; }
373.rhandle::after { content: ""; position: absolute; left: 0; right: 0; top: 4px; height: 2px; border-radius: 2px; background: var(--border); transition: background 0.15s; }
374.rhandle:hover::after, .rhandle.dragging::after { background: var(--accent); }
375body.resizing-v { cursor: row-resize; user-select: none; -webkit-user-select: none; }
376
377/* ── The status header ───────────────────────────────────────────
378 One row per question the user would otherwise have to open something to
379 answer. A row that can be acted on is a button that goes there. */
380/* The Status strip: fixed, pinned to the rail bottom, always on show. */
381.admin-status { display: flex; flex-direction: column; gap: 1px; flex: none; border-top: 1px solid var(--border); padding-top: 6px; }
382.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; }
383.astat-row.astat-id { gap: 4px; padding: 0; }
384/* A status row that carries a copy button beside it. The row stays the button
385 it always was -- it opens what it names -- and the copy sits outside it,
386 because a button inside a button is neither valid nor clickable. */
387/* The wrapper takes the RIGHT padding its sibling rows carry on `.astat-row`.
388 Without it the copy button ends flush with the panel's edge -- measured at a
389 0px gap where every other row stands 4px off it -- and read as clipped. */
390.astat-copyrow { display: flex; align-items: center; padding-right: 4px; }
391.astat-copyrow > .astat-row { flex: 1 1 auto; min-width: 0; }
392.astat-copyslot { flex: none; display: flex; align-items: center; }
393.astat-btn { width: 100%; background: none; border: none; text-align: left; }
394.astat-btn:hover { background: var(--bg-hover); }
395.astat-label { flex: none; color: var(--text-muted); }
396.astat-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
397.astat-aside { flex: none; font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary); white-space: nowrap; }
398/* The dot carries the state, so the row reads at a glance without being read. */
399.astat-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); }
400.astat-dot.ok { background: var(--ok); }
401.astat-dot.warn { background: var(--danger); }
402.astat-dot.off { background: var(--border-2); }
403.astat-lock { flex: none; font-size: var(--fs-3xs); color: var(--text-muted); }
404
405/* The sync row. Its chip (`#sync-chip`, built by js/sync.js) and its resting
406 line share one row and exactly one of them is ever showing, so the row's
407 height never changes and nothing in the strip moves under a thumb.
408 These rules were injected by sync.js while the chip was a pill in the top
409 bar; they are here now because the row they draw is in the markup, and
410 because this is where the strip's other ten rows are drawn. */
411.astat-sync { position: relative; }
412#sync-chip {
413 display: none;
414 flex: 1 1 auto;
415 align-items: center;
416 gap: 6px;
417 min-width: 0;
418 color: var(--text-secondary);
419}
420#sync-chip .stext { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
421#sync-chip[data-state="syncing"] { color: var(--accent); }
422#sync-chip[data-state="synced"] { color: var(--ok); }
423#sync-chip[data-state="off"] { color: var(--text-secondary); cursor: pointer; }
424/* A stall is not an error and not a success: something is wrong that the user
425 can fix, which is what --warn is for in the rest of the app. */
426#sync-chip[data-state="stalled"] { color: var(--warn); }
427/* The dot is the strip's dot, taking its colour from the state above it rather
428 than from a class, since the state is already on the element. */
429#sync-chip .sdot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
430#sync-chip[data-state="syncing"] .sdot { animation: syncpulse 1s ease-in-out infinite; }
431@keyframes syncpulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
432@media (prefers-reduced-motion: reduce) {
433 #sync-chip[data-state="syncing"] .sdot { animation: none; opacity: 1; }
434}
435
436/* The setup prompt. Until Daimond has a model it cannot answer anything, so the model row is
437 the one thing to do — it glows gently, on a slow cycle so it draws the eye without nagging.
438 Respects a user who has asked the OS for less motion. */
439@keyframes astat-pulse {
440 0%, 100% { background: var(--bg-hover); box-shadow: 0 0 0 0 rgba(210, 96, 79, 0.0); }
441 50% { background: var(--accent-soft); box-shadow: 0 0 0 2px rgba(210, 96, 79, 0.28); }
442}
443/* The Pro row reads its value as a link when there is an upgrade to take, so
444 "Upgrade to Pro" invites a click the way a bare status figure would not. */
445.astat-row.astat-pro-upgrade .astat-val { color: var(--accent-text); text-decoration: underline; }
446.astat-row.astat-pro-upgrade:hover .astat-val { filter: brightness(1.1); }
447.astat-row.astat-pulse { animation: astat-pulse 2.2s ease-in-out infinite; }
448.astat-row.astat-pulse .astat-val { color: var(--text-primary); font-weight: 600; }
449@media (prefers-reduced-motion: reduce) {
450 .astat-row.astat-pulse { animation: none; background: var(--accent-soft); box-shadow: 0 0 0 1px rgba(210, 96, 79, 0.4); }
451}
452
453/* The Admin panel: an overlay drawer that rises above the Status strip when
454 summoned and is gone again when finished. Hidden until #admin has .admin-open. */
455.admin-body {
456 position: absolute;
457 /* Inset from the rail's edges rather than flush with them: the rail clips
458 its overflow, so a drawer at left:0/right:0 has its side shadow cut away
459 and only the top and bottom survive. */
460 left: 5px; right: 5px;
461 bottom: 100%;
462 margin-bottom: 6px;
463 display: none;
464 flex-direction: column;
465 /* BOUNDED BY THE RAIL, NOT BY A NUMBER. This was `min(70vh, 540px)`, and 540
466 is the room above the Status strip on the window it was written on -- so on
467 any shorter one the drawer grew straight past the rail's top edge, and the
468 rail clips its overflow, so the drawer's own head went with it. Hit-tested
469 on 2026-08-28, sweeping the window from 640px to 1200px: at 840px and below
470 the drawer's × was not on the screen at all, and this is the one surface in
471 the app that covers the whole rail and has no other way out. At 640px the
472 drawer stood 157px above the rail's top.
473 `--admin-room` is that distance, measured by `boundDrawer` in js/daimond.js
474 whenever the rail or the strip changes size. It is also what makes the
475 strip's own height safe to change at all: a taller strip now makes the
476 drawer SHORTER instead of pushing it up into the rail's head, which is what
477 `#new-diamond-btn` was 14px from when the spend row was made permanent.
478
479 THE COMFORT CAPS ARE GONE WITH IT. `70vh` and `540px` both put the top edge
480 wherever the arithmetic landed, and at 940px of window that was inside
481 `#new-diamond-btn` -- half the button covered, its centre not, which is a
482 control that looks pressable and is not. One bound, at a structural
483 boundary, is what makes the top edge the same place at every height. */
484 max-height: var(--admin-room, 540px);
485 background: var(--bg-secondary);
486 border: 1px solid var(--border);
487 border-radius: 12px;
488 box-shadow: 0 8px 28px rgba(0, 0, 0, 0.22);
489 overflow: hidden;
490 z-index: 30;
491}
492.admin.admin-open .admin-body { display: flex; }
493/* The drawer's own header: the contextual title, and the one control that
494 dismisses it. Hidden in form mode, where the form supplies its own head. */
495.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); }
496.admin-drawer-head .admin-title { font-size: var(--fs-sm); }
497.admin.admin-form-mode .admin-drawer-head { display: none; }
498.admin-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 8px; }
499.admin-scroll::-webkit-scrollbar { width: 8px; }
500.admin-scroll::-webkit-scrollbar-track { background: transparent; }
501.admin-scroll::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 999px; }
502.admin-body .settings-section:last-child { margin-bottom: 0; }
503/* The rail no longer resizes its two panes: the Status strip is fixed and the
504 Admin panel overlays, so the drag handle has nothing to size. */
505#handle-split { display: none; }
506/* The rail is not a modal card: four money buttons in a column this narrow have
507 to wrap rather than shrink to an unreadable width. */
508.admin-body .credits-packs { flex-wrap: wrap; }
509.admin-body .credit-pack { flex: 1 1 calc(50% - 4px); }
510
511/* A view in the Admin body: the same furniture as a dialog, without the backdrop. */
512.admin-view-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 8px; }
513/* In the modal the hosted view names itself. The card used to carry a static
514 "Settings" heading instead, which was wrong the moment anything but the
515 settings view was hosted -- "Settings" sat over an "Add a mailbox" form. The
516 head travels with the view, so it is always the right words, and it wears the
517 card's heading size rather than the drawer's. */
518#settings-slot .admin-view-head { margin-bottom: 16px; }
519#settings-slot .admin-title { font-size: var(--fs-2xl); }
520#settings-slot .admin-back { font-size: var(--fs-2xl); padding: 2px 10px; }
521/* A built form brings a head of its own, which the slot's head already repeats
522 -- "Add a mailbox" twice, one above the other. The slot's own head wins: it is
523 the card's heading, and its × is wired to the form's cancel, so the promise
524 the caller is waiting on is still answered. */
525#settings-slot .admin-view .admin-view-head { display: none; }
526/* One way out, not three. The head's × closes the card (and cancels a form
527 properly); the trailing Close underneath said the same thing less well, and
528 sat below the fold of a long form. The phone has always hidden it. */
529#settings-modal #settings-close { display: none; }
530.admin-title { font-size: var(--fs-base); font-weight: 600; color: var(--text-primary); }
531.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); }
532.admin-back:hover { background: var(--bg-hover); color: var(--text-primary); }
533
534/* Home: the panel at rest. A call to action while Daimond cannot run, and the
535 account's own controls — which used to be a floating menu, and are one fewer
536 popup now that there is a panel to put them in. */
537.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; }
538.admin-cta:hover { background: var(--accent-hover); }
539/* ONE left edge for the whole drawer: 8px, which is where the rows land, and
540 the rows are what there are most of. Home stacks six kinds of line in one
541 narrow column -- section header, fingerprint, row, device, note, link -- and
542 each had arrived with its own horizontal padding, so the words started at
543 4, 6, 8 and 15px down a 190px column. Nothing there is centred and nothing is
544 ragged on purpose, so the eye reads the drift as four columns.
545 The CTA is left out: its label is centred in a filled button, so its padding
546 sets a width, not a margin. */
547.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; }
548.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); }
549.admin-item:hover { background: var(--bg-hover); }
550/* Keyboard focus, themed, matching the app's ring elsewhere (workspace.css).
551 These rows take real focus when a dialog closes and hands it back, and a
552 borderless button on a borderless panel showed nothing at all -- the caret was
553 somewhere in a list of eight identical lines. The accent is defined in all
554 three palettes, so the ring survives the light themes; a dark-only colour
555 would not. The offset stays inside the scroller's 8px padding. */
556/* The controls that had no themed ring of their own fell back to Chromium's,
557 which is drawn in rgb(16, 16, 16) -- a black ring on a near-black panel, and
558 clipped 1px besides by the scrollers they sit in. The offset is NEGATIVE
559 here, so the ring is painted inside the control's own box and no ancestor's
560 overflow can take it. */
561#user-row:focus-visible,
562#settings-btn:focus-visible,
563#spend-row:focus-visible,
564.astat-row:focus-visible,
565.models-prov-head:focus-visible {
566 outline: 2px solid var(--accent);
567 outline-offset: -2px;
568 border-radius: var(--radius-sm);
569}
570
571.admin-item:focus-visible {
572 outline: 2px solid var(--accent);
573 /* INSET, so the ring cannot be cut off. Drawn 2px OUTSIDE, the last item in
574 the drawer needed 4px the container did not have -- measured at -0.6px of
575 room, with overflow hidden and nothing to scroll -- so a keyboard user lost
576 the bottom of the ring on the one control at the edge, where it is hardest
577 to guess where you are. An inset ring is bounded by the button itself and
578 is therefore clip-proof at every spacing and in every palette. */
579 outline-offset: -2px;
580 background: var(--bg-hover);
581}
582.admin-item.danger { color: var(--danger); }
583.admin-item.danger:hover { background: var(--danger); color: var(--on-fill, #fff); }
584.admin-note { font-size: var(--fs-xs); color: var(--text-muted); padding: 4px 8px 8px; line-height: 1.5; }
585/* One heading idiom for the whole drawer. The account (user-name) menu heads its
586 groups with `.admin-sec` -- small, upper-case, muted -- while the Models,
587 Credits and Push views wore `.settings-section h3` at `--fs-lg`, so the two
588 menus read as two different systems. Inside the drawer the h3 takes the
589 `.admin-sec` metrics; the global `.settings-section h3` (below) still serves
590 the identity modal and anywhere else off the drawer. No left padding here: the
591 section already supplies the drawer's 8px, so the words land on the same edge
592 as every `.admin-sec` and `.admin-item`. */
593.admin-body .settings-section h3 {
594 font-size: var(--fs-2xs);
595 font-weight: 600;
596 letter-spacing: 0.04em;
597 text-transform: uppercase;
598 color: var(--text-muted);
599 margin: 0;
600 padding: 2px 0 6px;
601}
602/* The way through to the operator console. Drawn as one of the drawer's own
603 rows rather than as a button: it leads somewhere, it does not do anything.
604 So it spans the width they span and starts its words where they start theirs.
605 It is the one row here with a border, so it pays for that out of its padding:
606 7px of padding plus 1px of border is the 8px the borderless rows set. */
607.admin-console-link {
608 display: block;
609 margin: 2px 0;
610 padding: 6px 7px;
611 background: var(--bg-tertiary);
612 border: 1px solid var(--border-strong);
613 border-radius: var(--radius-sm);
614 color: var(--text-primary);
615 font-size: var(--fs-sm);
616 text-decoration: none;
617}
618.admin-console-link:hover { border-color: var(--accent); color: var(--accent-text); }
619
620/* ── Chats list (sessions) ───────────────────────────────────── */
621/* 4px of side padding, which is what `.railhead` and `.pptw-head` above it
622 already use. The two lists disagreed -- Diamonds sat on 6px and Chats on 0 --
623 so a Diamond tile's card was 6px narrower than a chat tile's and their two
624 columns of pause lights were 6px apart down the rail. On 4px every leading
625 edge in the rail starts in the same place: the global light, the section
626 labels, and both kinds of tile. */
627.session-list { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; gap: 6px; padding: 0 4px 2px; }
628
629.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; }
630.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); }
631.new-session-model:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
632.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; }
633.new-session-start:hover { background: var(--accent-hover); }
634
635.session-box { padding: 8px 9px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-secondary); cursor: pointer; flex: none; }
636.session-box:hover { border-color: var(--border-2); }
637.session-box.active { border-color: var(--accent); background: var(--accent-soft); }
638.session-box-header { display: flex; align-items: center; gap: 6px; }
639.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; }
640.session-box.active .session-box-name { color: var(--accent-text); }
641.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); }
642.session-box-rename:focus { outline: none; }
643/* The cog, and the closer cross notes4 asked back beside it. Both always
644 visible, never on hover: the old cross was `opacity: 0` until the pointer
645 arrived, which is no control at all on a phone -- and it put the one
646 irreversible act on the tile's easiest pixel. 20px square each, the same
647 box the pause light opposite them wears, so the row sits on one line and
648 one baseline.
649 The cross sits OUTSIDE the cog (rightmost in source order, which this
650 flex row draws left to right) -- the same slot every other closer in the
651 app holds, from a panel head's `.panel-close` to the tile dialog's own
652 `x`. A hand that has learned "the × is at the row's far edge" finds it
653 there on a tile too; the cog, one step in, is for settings that are not
654 final. */
655.tile-cog, .tile-x {
656 flex: none; width: 20px; height: 20px; padding: 0;
657 display: flex; align-items: center; justify-content: center;
658 background: none; border: none; border-radius: var(--radius-sm);
659 line-height: 0; color: var(--text-muted);
660}
661.tile-cog:hover { color: var(--text-primary); background: var(--bg-hover); }
662.tile-cog:focus-visible, .tile-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
663/* Danger on hover, not at rest: the same treatment `.queue-x` wears, so a
664 cross that ends something for good reads the same wherever it appears. */
665.tile-x:hover { color: var(--danger); background: var(--bg-hover); }
666.session-box-meta { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 3px; font-family: var(--font-mono); }
667.session-box-ctx, .session-box-cost, .session-box-time { font-size: var(--fs-2xs); color: var(--text-muted); }
668
669/* ── Simple and Max ──────────────────────────────────────────────
670 EVERY RULE FOR THE TWO VIEWS LIVES IN THIS BLOCK. Put one beside its own
671 component and nobody can answer "what does Simple hide?" without grepping,
672 and the answer rots silently the first time a class is renamed.
673
674 The two views are not "more" and "less". They differ in WHERE A CONTROL LIVES
675 relative to the thing it controls:
676
677 Simple one object at a time, controls inside it -- open the tile, change
678 it, close it.
679 Max many objects at once, controls hoisted onto the tile, so they can
680 be COMPARED and changed without navigating.
681
682 Comparison is the job Max exists for: "which of my six Diamonds is on the
683 expensive model" cannot be answered by opening six dialogs. That also says
684 what does NOT belong on a Max tile -- anything nobody compares and nobody
685 changes mid-flow stays in the dialog however much room there is. Delete,
686 rename, the triggered-action editor and scope are all in that group.
687
688 Two invariants, both asserted in dev/verify_view.mjs:
689
690 1. Simple hides INFORMATION, never AFFORDANCES. Everything reachable in Max
691 is reachable in Simple within one more click, and the way in -- the cog
692 -- is always visible. Hide the route as well as the detail and the
693 reader concludes the app is broken.
694 2. Simple never hides a warning, an error, or a SPENDING CONTROL. Notes2
695 opens on the Transparent Control Principle; a display preference that
696 hid a traffic light would contradict the app's own first rule.
697
698 Hidden with CSS rather than by building a different tile, so every handler is
699 wired either way -- a hidden pulldown still answers `DaimondModels.pick`, so
700 Start on a Simple tile starts on the model the tile holds rather than on
701 nothing. `display: none` also takes an element out of the tab order, so this
702 costs nothing in keyboard terms.
703
704 What Simple does NOT hide: the pause light, the name, the queue badge, the
705 pending-fold badge and the tags. The first three are money or work in
706 flight, and the last two are how a Diamond is found. */
707.session-box[data-detail="simple"] .tile-model,
708.session-box[data-detail="simple"] .tile-worker-row,
709.session-box[data-detail="simple"] .tile-model-chip,
710.session-box[data-detail="simple"] .tile-meter,
711.session-box[data-detail="simple"] .session-box-ctx,
712.session-box[data-detail="simple"] .session-box-time { display: none; }
713/* A meta row whose every child is hidden still costs its top margin. Its own
714 rule, because an unsupported `:has()` would otherwise drop the whole list
715 above with it. */
716.session-box[data-detail="simple"] .session-box-meta:not(:has(.diamond-pending)) {
717 display: none;
718}
719/* Per-object spend is a Max thing and a new one: "which Diamond is eating the
720 money" is comparable across objects and unanswerable anywhere else in the
721 app. It is information, not a control, so hiding it in Simple does not touch
722 invariant 2 -- the light and the global spend readout are both still there. */
723.session-box[data-detail="simple"] .session-box-spend { display: none; }
724.session-box-spend { font-size: var(--fs-2xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
725/* The Diamond tile's meter row: context bar · relative time · spend, in the
726 shape of the meter on an ordinary chat tile (`.tile-meter`) minus the token
727 count. Shown in Max, hidden in Simple with the rest of the comparable
728 detail -- it is information, not a control, so invariant 2 is untouched.
729 A missing context bar means the daimon has spent no tokens or its model
730 publishes no window; the row still carries time and spend, so it renders
731 whenever either exists. */
732.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); }
733.session-box[data-detail="simple"] .diamond-meter { display: none; }
734/* Tags, on a row of their own. They used to sit inline in `session-box-meta`,
735 which read as though they belonged to the version number and the model chip,
736 and a heavily-filed Diamond pushed the rest of the row off the tile.
737
738 NOT HIDDEN IN SIMPLE. The rule above says so in as many words -- "What Simple
739 does NOT hide: ... the pending-fold badge and the tags ... the last two are
740 how a Diamond is found" -- and moving them out of `meta` came within one line
741 of silently reversing it: the row inherited nothing, so a `display: none` was
742 written for it and nothing in the suite went red. Finding a Diamond by its tag
743 is the one thing Simple must not take away, because Simple is the view with
744 fewer other ways to tell two tiles apart.
745
746 EVERY TAG RENDERS and the row SCROLLS SIDEWAYS rather than wrapping or
747 truncating with a +N chip: an unrendered tag cannot be clicked into a filter,
748 so the overflow chip hid the one thing a tag exists for. The row does not
749 wrap, and `scrollbar-width: none` keeps a scrollbar from stealing a row of
750 height on a narrow rail -- the chips say "more this way" by being cut at the
751 edge, which is the affordance a horizontal clip gives for free. */
752.session-box-tags {
753 display: flex; flex-wrap: nowrap; gap: 4px; margin-top: 3px;
754 overflow-x: auto; overflow-y: hidden;
755 scrollbar-width: none; /* Firefox */
756 -ms-overflow-style: none; /* legacy Edge */
757}
758.session-box-tags::-webkit-scrollbar { display: none; } /* Chromium/Safari */
759.session-box-tags .tag-chip { flex: none; } /* keep chips their own width */
760
761/* Simple is a DESIGN, not a subtraction. Take the room the hidden rows leave
762 and give it to what is left, or the tile reads as the dense one with holes in
763 it rather than as a calm one. Bigger target, fewer things in it. */
764[data-view="simple"] .session-box[data-detail="simple"] {
765 padding-top: 10px;
766 padding-bottom: 10px;
767}
768[data-view="simple"] .session-box[data-detail="simple"] .session-box-name { font-size: var(--fs-md); }
769
770/* THE STATUS HEADER IS NOT TRIMMED IN SIMPLE, and the reason is invariant 1.
771 Simple hid three of its rows for a while -- Tools, Version and the machine
772 workspace -- on the grounds that seven rows of mostly-static reassurance is
773 the most intimidating thing on a first run. That was wrong, and wrong by the
774 rule written above it: every one of those rows is a BUTTON. Version opens the
775 release history and the transparency log; Tools opens the panel whose whole
776 purpose is telling a user what this app can do, which they will never ask for
777 if they do not know it exists. Hiding them removes the ROUTE, not the detail,
778 which is the one thing Simple may never do.
779
780 `verify_release` caught it by failing to click a row that had quietly gone. */
781
782/* ── Center panel ────────────────────────────────────────────── */
783.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); }
784.ctitle { font-size: var(--fs-base); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
785/* ── The crystal face ────────────────────────────────────────────
786 The AI panel has two faces: a conversation, and a Diamond's crystal. The
787 crystal is a document the user owns, so the panel takes the mark beside the
788 name and squares its corners -- everything else in the app is rounded, so
789 square IS the distinction, in both skins (the warm skin only enlarges
790 --radius-lg, which this beats on specificity). */
791.chead-mark { width: 18px; height: 18px; flex: none; }
792/* The title takes the slack, so the mark sits against it rather than being
793 spread away from it by the head's space-between. */
794.panel.ai .chead .ctitle { flex: 1 1 auto; min-width: 0; }
795.panel.ai.crystal-face { border-radius: 0; }
796.chead-right { display: flex; align-items: center; gap: 8px; flex: none; }
797/* The two faces of a Diamond, as one segmented control rather than two buttons:
798 they are one choice, and a pair of separate buttons reads as two actions. */
799.dview { display: flex; flex: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
800.dview-btn {
801 background: var(--bg-tertiary); border: 0; color: var(--text-muted);
802 padding: 3px 10px; font-size: var(--fs-2xs); white-space: nowrap;
803}
804.dview-btn + .dview-btn { border-left: 1px solid var(--border); }
805.dview-btn:hover { color: var(--text-primary); }
806.dview-btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-text); }
807.dview-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
808.cmeter { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary); white-space: nowrap; }
809.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; }
810.chat-model-select:hover { border-color: var(--accent); color: var(--text-primary); }
811/* A pulldown, so `:focus-visible` -- see the note at `.settings-select`. */
812.chat-model-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
813
814.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; }
815.chat-input-bar { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); }
816#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; }
817#chat-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
818#chat-input::placeholder { color: var(--text-muted); }
819/* Send, and the walk back up your own questions. Both are square and exactly as
820 tall as the input at rest — 21px of line + ~9px of vertical padding + 2px of
821 border — so the row reads as one control rather than a box with taller buttons
822 beside it. The bar aligns to flex-end, so they stay on the baseline as the
823 input grows. */
824#chat-send, #chat-jump, #chat-end {
825 box-sizing: border-box;
826 height: 32px;
827 width: 32px;
828 flex-shrink: 0;
829 border-radius: var(--radius);
830 font-size: var(--fs-base);
831 display: flex;
832 align-items: center;
833 justify-content: center;
834}
835#chat-send { background: var(--accent); color: var(--on-fill, #fff); border: none; }
836#chat-send:hover { background: var(--accent-hover); }
837#chat-send:disabled { opacity: 0.4; cursor: default; }
838#chat-send.stop { background: var(--danger); }
839#chat-send.stop:hover { filter: brightness(1.1); }
840#chat-jump, #chat-end { background: var(--bg-tertiary); border: 1px solid var(--border-strong); color: var(--text-secondary); }
841/* The two jumps draw an SVG chevron now, not a text arrowhead, so `font-size` no
842 longer decides how big the mark is -- the glyph does. Stated here so the touch
843 size and the mark size are set in the same place and cannot drift apart. */
844#chat-jump .ic, #chat-end .ic { width: 16px; height: 16px; }
845#chat-jump:hover, #chat-end:hover { background: var(--bg-hover); color: var(--text-primary); }
846/* Chat messages */
847/* An answer belongs to the question above it, so it sits close to it; the air
848 goes between turns instead, where it separates things that are separate.
849 (Each message also carries 22px of bottom padding — the gutter the copy
850 button lives in — so these margins are on top of that, not instead of it.) */
851.chat-msg { margin-bottom: 6px; }
852.chat-msg-user { margin-bottom: 2px; }
853.chat-msg-user { text-align: right; }
854.chat-msg-user .chat-msg-content {
855 display: inline-block;
856 background: var(--accent-soft);
857 color: var(--accent-text);
858 border-radius: var(--radius-lg);
859 padding: 8px 14px;
860 max-width: 82%;
861 text-align: left;
862}
863.chat-msg-assistant .chat-msg-content { max-width: 92%; }
864/* A turn the browser closed in the middle of: what arrived is shown, dimmed and badged, with a
865 Continue button that runs it again. */
866.chat-msg.interrupted .chat-msg-content { opacity: 0.85; }
867.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); }
868.ti-label { font-size: var(--fs-xs); color: var(--warn); flex: 1; min-width: 0; }
869.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; }
870.ti-continue:hover { background: var(--accent-hover); }
871.chat-msg-content p { margin: 0 0 8px; }
872.chat-msg-content p:last-child { margin-bottom: 0; }
873.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; }
874.chat-msg-content code { font-family: var(--font-mono); font-size: var(--fs-sm); background: var(--bg-tertiary); padding: 2px 6px; border-radius: 3px; }
875.chat-msg-content pre code { background: none; padding: 0; }
876.chat-msg-content ul, .chat-msg-content ol { margin: 8px 0; padding-left: 24px; }
877.chat-msg-content h1, .chat-msg-content h2, .chat-msg-content h3 { margin: 12px 0 6px; }
878
879/* ── Queued messages ────────────────────────────────────────────
880 What you typed while the answer was still arriving. It reads as a user
881 message that has not happened yet: the same bubble on the same side, dashed
882 and dimmed, under a line saying what it is waiting for. Deliberately NOT
883 .chat-msg-user — that class is how a turn is counted. */
884.chat-queued { margin-top: 10px; }
885.chat-queued-head { font-size: var(--fs-2xs); color: var(--text-muted); text-align: right; margin-bottom: 4px; }
886.chat-msg-queued { position: relative; text-align: right; margin-bottom: 4px; padding-right: 20px; }
887.chat-msg-queued .chat-msg-content {
888 display: inline-block;
889 background: none;
890 color: var(--text-secondary);
891 border: 1px dashed var(--border-2);
892 border-radius: var(--radius-lg);
893 padding: 7px 13px;
894 max-width: 82%;
895 text-align: left;
896 cursor: text;
897}
898.chat-msg-queued .chat-msg-content:hover { border-color: var(--accent); color: var(--text-primary); }
899/* Withdraw a message that has not been sent. Centred on the BUBBLE: a queued
900 message is still a .chat-msg, so it inherits the copy button's gutter even
901 though nothing is ever drawn in it, and a plain 50% centred the closer on the
902 bubble PLUS that gutter -- half a gutter low, level with the bubble's last
903 line rather than with the bubble. */
904.queue-x {
905 position: absolute;
906 top: calc((100% - var(--copy-gutter, 0px)) / 2);
907 right: 0;
908 transform: translateY(-50%);
909 background: none;
910 border: none;
911 color: var(--text-muted);
912 font-size: var(--fs-lg);
913 line-height: 1;
914 padding: 0 4px;
915 cursor: pointer;
916}
917.queue-x:hover { color: var(--danger); }
918
919/* The fold notice — Daimond's own edit of the conversation, drawn where it
920 happened. Left-aligned and full width, unlike the right-aligned bubbles above:
921 it is not something anyone SAID, it is a note about the thread itself, and
922 giving it a speaker's shape would read as the model summarising its own work.
923 Tokens throughout, because an inline style does not survive a palette. */
924.chat-msg-compacted { margin: 10px 0; }
925.chat-msg-compacted .chat-queued-head { text-align: left; }
926.chat-msg-compacted .chat-msg-content {
927 background: var(--bg-tertiary);
928 color: var(--text-secondary);
929 border: 1px dashed var(--border-2);
930 border-left: 3px solid var(--border-2);
931 border-radius: var(--radius-sm);
932 padding: 8px 12px;
933 font-size: var(--fs-xs);
934 line-height: 1.5;
935 white-space: pre-wrap;
936}
937
938/* THE FOLD IS A BOUNDARY, NOT A MESSAGE.
939 *
940 * A tester read a long chat as losing its history. Nothing was: the conversation had folded,
941 * the app said so, and the sentence sat at the bottom of a transcript that still showed every
942 * earlier message in full. See `appendCompacted` in www/js/daimond.js for the whole reasoning.
943 *
944 * So it is drawn as a rule across the thread. The rule is the signal; the ink above it is
945 * NOT reduced, because dimming the words would be the app hiding a conversation it has just
946 * been accused of losing. What marks them is a line down the side, in the same border the
947 * notice wears, so the eye can follow the region back to the rule that names it. */
948.chat-fold-line { position: relative; padding-top: 14px; }
949.chat-fold-line::before {
950 content: '';
951 position: absolute;
952 top: 4px; left: 0; right: 0;
953 border-top: 1px dashed var(--border-2);
954}
955.chat-fold-boundary {
956 margin-top: 6px;
957 padding: 0 2px;
958 color: var(--text-secondary);
959 font-size: var(--fs-xs);
960 line-height: 1.5;
961}
962.chat-above-fold {
963 border-left: 2px solid var(--border-2);
964 padding-left: 8px;
965 margin-left: -10px;
966}
967
968/* How the turn ended — FURNITURE, and the styling is the whole of the
969 not-crying-wolf rule (dev/CONTRACT_CLAIMS.md §3). One quiet line in the
970 register of a timestamp: no box, no rule, no colour that claims a fault. An
971 app that appends a warning to every turn teaches its reader to skip the one
972 that mattered, and that is exactly the failure this line exists to prevent.
973
974 `.ended-notice` is the promotion, and it is deliberately NOT the error or the
975 alert styling — neither --danger nor --warn appears here. What changes is
976 contrast and a thin rule: one step nearer the reader, which is what a notice
977 is, without a colour that says something broke. `refused > 0`, `failed > 0`
978 or a missing path is the only thing that reaches it. */
979.chat-msg-ended {
980 margin: 2px 0 12px;
981 font-size: var(--fs-2xs);
982 line-height: 1.5;
983 color: var(--text-muted);
984}
985.chat-msg-ended .end-paths {
986 margin-top: 2px;
987 font-family: var(--font-mono, ui-monospace, monospace);
988 word-break: break-all;
989}
990.chat-msg-ended.ended-notice {
991 color: var(--text-secondary);
992 border-left: 2px solid var(--border-strong);
993 padding-left: 8px;
994}
995/* The same fact on a worker's tile, which is not a thread: it rides with the
996 vision note, in the app's own voice rather than the worker's. */
997.acard .anote.aended { color: var(--text-muted); }
998.acard .anote.aended.ended-notice {
999 color: var(--text-secondary);
1000 border-left: 2px solid var(--border-strong);
1001 padding-left: 6px;
1002}
1003
1004/* Diamonds rail list — reuses .session-box styling via the shared class. A long
1005 list scrolls rather than squeezing the chats out of the pane above it. */
1006.diamond-list { display: flex; flex-direction: column; gap: 6px; padding: 0 4px 6px; flex: 0 1 auto; min-height: 0; overflow-y: auto; }
1007
1008/* Find a Diamond: a search box that is always there, and the standing tag pool
1009 beneath it. */
1010.diamond-find { display: flex; flex-direction: column; gap: 5px; padding: 0 4px; flex: none; }
1011.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; }
1012.diamond-search:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1013.diamond-search::placeholder { color: var(--text-muted); }
1014.diamond-search::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
1015/* The tag filter. A column: the row that names it, the pool of tags behind
1016 that, then the controls that are not tags. The agents panel puts a single chip
1017 straight in here, and a column of one looks the same as the row it used to
1018 be. */
1019.diamond-filter { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
1020.tagf-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
1021/* The row the pool sits behind. Same place, same size and same ink as the
1022 "No tags yet" line it replaces the moment a tag exists, so the question "is
1023 there a filter here?" gets one quiet answer either way -- and the chips are
1024 one click behind it rather than permanently in front of the two lists.
1025
1026 The whole row is the target. A rail is 250px of mostly empty space to the
1027 right of a short label, and a hit area that ends at the last letter is a
1028 control that gets missed. */
1029.tagf-toggle {
1030 display: flex;
1031 align-items: center;
1032 gap: 6px;
1033 align-self: stretch;
1034 background: none;
1035 border: none;
1036 padding: 1px 2px;
1037 text-align: left;
1038 font-size: var(--fs-2xs);
1039 line-height: 1.35;
1040 color: var(--text-muted);
1041 cursor: pointer;
1042}
1043.tagf-toggle:hover { color: var(--text-secondary); }
1044.tagf-toggle:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
1045/* Two borders on an empty box, turned 45°: a chevron that is ink rather than a
1046 character. Typed, it would be one missing glyph away from saying nothing, in
1047 an app that ships eight languages -- and it would ride in the row's text,
1048 where the tag count is read. */
1049.tagf-chev {
1050 flex: none;
1051 width: 5px;
1052 height: 5px;
1053 margin-bottom: 1px; /* the turned square hangs low against the cap height */
1054 border-right: 1.5px solid currentColor;
1055 border-bottom: 1.5px solid currentColor;
1056 transform: rotate(-45deg);
1057 transition: transform 0.15s;
1058}
1059.tagf-toggle[aria-expanded="true"] .tagf-chev { transform: rotate(45deg); }
1060/* The pool: every tag in use, each chip cycling in place. One click behind the
1061 row above, which is what a filter nobody has to go looking for costs when the
1062 filing system is real -- the row is always there, the chips are not.
1063
1064 Capped and scrolled past that. The rail's two lists live on what this leaves,
1065 so a vocabulary of thirty tags must not be allowed to take the rail off a
1066 reader who came for the Diamonds. A chip row is a 1.6em line box between two
1067 1px borders, and the cap is three and a HALF of them plus the flex gaps --
1068 stated in em against the pool's own font-size, so it follows the type scale
1069 rather than pinning one.
1070
1071 The half row is the point of the number. A cap of exactly three would end on
1072 a row boundary, and thirty tags would look like nine: three tidy rows and no
1073 reason to suspect the rest. A row cut through the middle is the only thing
1074 in the box that says there is more of it. Under four rows the pool is shorter
1075 than the cap anyway, so nothing is cut when there is nothing to hide. */
1076.tagf-pool {
1077 display: flex;
1078 flex-wrap: wrap;
1079 align-items: center;
1080 gap: 4px;
1081 align-self: stretch;
1082 font-size: var(--fs-3xs);
1083 max-height: calc(5.6em + 19px);
1084 overflow-y: auto;
1085 overscroll-behavior: contain; /* on the phone the drawer must not scroll with it */
1086}
1087/* Closed, with a filter on: the chips that are HOLDING it, standing on their
1088 own. A filter the reader cannot see is a rail hiding Diamonds for a reason it
1089 will not give. Same chips, same cycle, same order -- only the ones doing
1090 something -- so one tag filtering out of thirty costs one chip, not thirty.
1091 Capped like the pool: a filter can be built out of more tags than a rail can
1092 spare, and the state must be reachable rather than merely present. */
1093.tagf-active {
1094 align-self: stretch;
1095 font-size: var(--fs-3xs);
1096 max-height: calc(5.6em + 19px);
1097 overflow-y: auto;
1098 overscroll-behavior: contain;
1099}
1100/* The app's scrollbar is 14px, which is a fifth of this box's own height and a
1101 twentieth of the rail's width. */
1102.tagf-pool::-webkit-scrollbar,
1103.tagf-active::-webkit-scrollbar { width: 8px; }
1104/* Off, a pool chip is a plain chip, so only the cursor says it is a control. */
1105button.tag-pool { cursor: pointer; }
1106/* What is left below the pool once each chip holds its own state: how the
1107 wanted tags combine, and the one click that puts the whole filter down. */
1108.tagf-ctl { gap: 8px; }
1109/* No tags anywhere: one muted line where the chips would be, so an empty
1110 filing system does not look like a missing one. */
1111.rail-tag-hint { font-size: var(--fs-2xs); line-height: 1.35; color: var(--text-muted); }
1112
1113/* Crystal view — the Diamond command surface (steer + fold). */
1114.crystal-view { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
1115.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; }
1116.crystal-body .crystal-empty { color: var(--text-muted); font-style: italic; }
1117.crystal-controls { border-top: 1px solid var(--border); padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
1118
1119/* THE COMPOSER SITS BELOW THE CRYSTAL, not above it.
1120 `.chat-input-bar` comes BEFORE `.crystal-view` in the document, because it
1121 was written when only the chat thread ever used it and the crystal carried a
1122 steer box of its own. That box is gone and the composer is shared, so on the
1123 crystal face the source order puts the box at the top of the page -- above
1124 the document it is meant to sit under. Ordered explicitly rather than moved
1125 in the markup, because the chat face wants the source order it already has. */
1126.panel.ai.crystal-face .crystal-view { order: 1; }
1127.panel.ai.crystal-face .chat-input-bar { order: 2; }
1128
1129/* ── Full screen: the crystal, alone ─────────────────────────────────
1130 A capp is an application, and an application inside a panel inside an app
1131 reads on a phone as an app inside an app. `[data-cfull]` on the root element
1132 takes the app chrome away and leaves the page: the rail, the top bar, the
1133 dock, the other stage panels, the composer and the crystal's own bar.
1134
1135 The mode is a USER's, and only a user's -- see `setCrystalFull` in
1136 js/daimond.js. Nothing here is a media query and nothing enters it by size:
1137 a phone gets the same panel it always had until somebody presses the button.
1138
1139 WHAT IS NOT DONE HERE, deliberately: the panel is not lifted out of `#app`
1140 into a fixed, inset-0 box. `#app` already carries the safe-area insets in its
1141 own padding, so leaving the panel inside it is what keeps the way out clear
1142 of the notch; a fixed overlay would have to restate all four insets and would
1143 be wrong the first time one of them changed. */
1144:root[data-cfull] .topbar,
1145:root[data-cfull] #panel-rail,
1146:root[data-cfull] #handle-rail,
1147:root[data-cfull] .dock,
1148:root[data-cfull] .stage > .phandle,
1149:root[data-cfull] .stage > .panel:not(.ai),
1150:root[data-cfull] .panel.ai .chat-input-bar,
1151:root[data-cfull] .panel.ai .crystal-bar,
1152:root[data-cfull] .panel.ai .crystal-controls,
1153:root[data-cfull] .panel.ai #diamond-view,
1154:root[data-cfull] .panel.ai .chead-right > *:not(.cfull-btn) { display: none !important; }
1155
1156/* The one panel left takes the stage, whatever inline width the layout engine
1157 last measured it at. */
1158:root[data-cfull] .stage > .panel.ai {
1159 flex: 1 1 auto !important;
1160 width: auto !important;
1161 display: flex !important;
1162}
1163
1164/* The page runs to the panel's own edges. The gutter is chrome too.
1165 Only when there IS a page: `:has()` scopes this exactly as crystal.css scopes
1166 the flex column, so the built-in view -- which is prose in our own document --
1167 keeps the margin every other panel's text has. */
1168:root[data-cfull] .panel.ai .crystal-body:has(#crystal-frame-wrap) { padding: 0; }
1169
1170/* The toggle. Two glyphs, one shown at a time, and a word beside the glyph
1171 ONLY while the mode is on: coming back is the thing a reader has to be able
1172 to do without working out what an icon means, and it is the one moment when
1173 there is room for the word because every other chip in the row has gone. */
1174.cfull-btn { display: inline-flex; align-items: center; gap: 5px; padding: 3px 6px; }
1175/* An explicit ring rather than the browser's own. In full screen this is the
1176 only control on the app, so where the keyboard is has to be legible against
1177 the accent ground it wears there. */
1178.cfull-btn:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 2px; }
1179.cfull-btn .ic { width: 16px; height: 16px; }
1180.cfull-btn .ic-cfull-out,
1181.cfull-txt { display: none; }
1182:root[data-cfull] .cfull-btn .ic-cfull-in { display: none; }
1183:root[data-cfull] .cfull-btn .ic-cfull-out { display: block; }
1184:root[data-cfull] .cfull-txt { display: inline; }
1185/* On in the sense the header's other latched chip is on, so the mode is
1186 readable from the control as well as from the screen behind it. */
1187:root[data-cfull] .cfull-btn { background: var(--accent); border-color: var(--accent); color: var(--on-fill, #fff); }
1188
1189/* Artefact strip: what this Diamond produced or consulted. A count that hides at
1190 zero and opens a list, rather than a list that is always there taking room
1191 from the crystal above it and the steer box below. */
1192.arte-strip { display: block; width: 100%; text-align: left; background: none; border: none;
1193 color: var(--text-muted); font-size: var(--fs-xs); font-family: var(--font-mono);
1194 padding: 4px 2px; margin: 2px 0; cursor: pointer; }
1195.arte-strip:hover { color: var(--text-secondary); }
1196.arte-list { margin: 0 0 6px;
1197 border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-tertiary); }
1198.arte-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px 6px;
1199 min-height: var(--attach-row-h); box-sizing: border-box;
1200 border-bottom: 1px solid var(--border); }
1201.arte-row:last-child { border-bottom: none; }
1202.arte-kind { flex-shrink: 0; font-size: var(--fs-3xs-2xs); font-family: var(--font-mono);
1203 color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; min-width: 34px; }
1204.arte-open { flex: 1; min-width: 0; text-align: left; background: none; border: none;
1205 color: var(--text-primary); font-size: var(--fs-xs-sm); font-family: var(--font-mono);
1206 padding: 2px 0; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1207button.arte-open:hover { color: var(--accent-text); }
1208/* A tile for something that cannot be opened. Not a button, because there is
1209 nothing to press -- and the reason is TEXT, not a hover title, because a
1210 tooltip is unavailable to a touch and an item that is silently inert on a
1211 phone is exactly the empty-folder failure this exists to end. Wanted twice
1212 over: ATTACH_CONTRACT.md §7 for an attachment from a workspace that is not
1213 open, and §9 for a Diamond in the trash. */
1214span.arte-open { cursor: default; }
1215.arte-open.away { color: var(--text-muted); font-style: italic; }
1216.arte-why { flex-basis: 100%; order: 9; padding: 0 0 3px 40px;
1217 font-size: var(--fs-3xs-2xs); color: var(--text-muted); line-height: 1.4; }
1218.arte-use, .arte-drop { flex-shrink: 0; background: none; border: none; cursor: pointer;
1219 color: var(--text-muted); font-size: var(--fs-sm); line-height: 1; padding: 2px 5px; }
1220.arte-use:hover { color: var(--accent); }
1221.arte-drop:hover { color: var(--danger, #e5534b); }
1222
1223/* Note or Read, toggleable on the tile (ATTACH_CONTRACT.md §6). Note is the
1224 cheaper of the two by a wide margin, so it carries no colour of its own;
1225 Read spends the user's money and says so by picking up the accent, the same
1226 way a lit `.on` control does everywhere else in this app. */
1227.attach-state { flex-shrink: 0; background: none; border: 1px solid var(--border-2);
1228 border-radius: 999px; cursor: pointer; color: var(--text-secondary);
1229 font-size: var(--fs-3xs-2xs); line-height: 1.6; padding: 0 7px; }
1230.attach-state:hover { border-color: var(--accent); }
1231.attach-state.read { color: var(--accent-text); border-color: var(--accent); background: var(--accent-soft); }
1232
1233/* A chat's queue for its next turn, above the composer -- ATTACH_CONTRACT.md
1234 §5: "showing exactly what the crystal footer shows". Reusing `.arte-list`
1235 is what makes that literally true rather than a promise to keep two things
1236 in step; this rule only adds the margin a footer above a composer needs
1237 that a list under a strip button did not. */
1238.chat-attach-strip { margin: 0 0 6px; }
1239
1240/* ── A chat's footer is TWO GROUPS, and the difference is the point ──
1241 The workspace is a COLLECTIVE claim: take a folder out and the fence shrinks.
1242 So it is drawn as one bounded box with a rail down its edge -- one thing made
1243 of parts -- headed by the app's own words for it, `astat.workspace_browser`
1244 and `astat.workspace_native`, the same words the account strip uses.
1245 The attachments are individuals: a header and a plain list, no box, because
1246 dropping one changes nothing but itself.
1247 The strip's own frame comes off for this. A box inside a box reads as one
1248 thing with a mistake in it, and the two groups now carry their own edges. */
1249.chat-attach-strip { border: none; background: none; }
1250.attach-group + .attach-group { margin-top: 10px; }
1251/* Compounded with `.attach-head`, which is the class it also wears and which sets
1252 `justify-content: flex-end` for a band that is nothing but controls. Equal
1253 specificity would leave the winner to be decided by which rule came later in
1254 this file, and a group's NAME belongs at the left however the file is ordered. */
1255.attach-head.attach-group-head { justify-content: space-between; border-bottom: none;
1256 padding: 1px 3px 3px; }
1257.attach-group-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1258 font-size: var(--fs-3xs-2xs); font-family: var(--font-mono); color: var(--text-muted);
1259 text-transform: uppercase; letter-spacing: .04em; }
1260
1261/* ONE LIST since 2026-08-24, and therefore no accent rail down its edge. The
1262 rail said "everything in this box is in the fence", which was true while the
1263 box held only marked folders and stopped being true the moment the two groups
1264 became one: it would have bracketed attachments that grant nothing. The claim
1265 is now made once, on the row that makes it -- the `Workspace` pill below --
1266 rather than twice, once wrongly.
1267 The cap is `--attach-cap`, the FOOTER's own, because this is the whole footer
1268 and not the upper of two groups. */
1269.ws-body { border: 1px solid var(--border-strong);
1270 border-radius: var(--radius); background: var(--bg-tertiary);
1271 max-height: calc(var(--attach-cap) * var(--attach-row-h) + var(--attach-peek));
1272 overflow-y: auto; scrollbar-gutter: stable; scrollbar-width: thin; }
1273.ws-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
1274.ws-body::-webkit-scrollbar { width: 10px; }
1275.ws-body::-webkit-scrollbar-track { background: var(--bg-secondary); border-radius: 5px; }
1276.ws-body::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 5px;
1277 border: 2px solid var(--bg-secondary); }
1278/* The sentence for the empty fence. It wraps, it is the tallest thing this
1279 group is ever drawn at, and it is deliberately not a hint about a button:
1280 what it says is what the chat can reach. */
1281.ws-empty { padding: 7px 9px; line-height: 1.45; }
1282
1283/* One list since 2026-08-24, so there is no second group to make room for and
1284 no `.two` arithmetic to do. What tells a marked folder from an attached file
1285 is the row itself: the `Workspace` pill below, pressed or not. */
1286
1287/* The workspace mark, on a folder's tile in either group: SQUARE, where
1288 Note/Read is round. Two independent things sit side by side here, and one of
1289 them decides what the agent may change -- telling them apart must not depend
1290 on reading two short words, or on telling two accent tints apart. */
1291.attach-ws { flex-shrink: 0; background: none; border: 1px solid var(--border-2);
1292 /* 3px, not `--radius-sm`: at this height every radius the app has still reads
1293 as a pill beside the round one, and the two controls must be told apart
1294 without reading either word. */
1295 border-radius: 3px; cursor: pointer; color: var(--text-secondary);
1296 font-size: var(--fs-3xs-2xs); line-height: 1.6; padding: 0 7px; }
1297.attach-ws:hover { border-color: var(--accent); }
1298.attach-ws.on { color: var(--accent-text); border-color: var(--accent); background: var(--accent-soft); }
1299
1300/* The chrome both footers wear: the view toggle and `+`, above the tiles and
1301 outside the scroll, because chrome that scrolls away is chrome nobody finds
1302 a second time. */
1303.attach-head { display: flex; align-items: center; justify-content: flex-end; gap: 4px;
1304 padding: 3px 5px; border-bottom: 1px solid var(--border); }
1305/* `--border-strong`, the control boundary, NOT `--border-2`, which divides one
1306 region from another. A control that borrows the divider token disappears at
1307 its own edge in the paler palettes, and this is the rule verify_theme keeps:
1308 the failure it catches is a new control written by copying an old one. */
1309.attach-view-btn { background: none; border: 1px solid var(--border-strong); border-radius: 999px;
1310 cursor: pointer; color: var(--text-secondary); font-size: var(--fs-3xs-2xs);
1311 line-height: 1.6; padding: 0 9px; }
1312.attach-view-btn:hover { border-color: var(--accent); color: var(--accent-text); }
1313/* It carries a WORD now, so it is shaped like the pill beside it rather than
1314 like an icon: a bare `+` said nothing, and the two of them a few pixels apart
1315 -- one granting a fence, one not -- read as the same button. */
1316.attach-add { background: none; border: 1px solid var(--border-strong); border-radius: 999px;
1317 cursor: pointer; color: var(--text-secondary); font-size: var(--fs-3xs-2xs);
1318 line-height: 1.6; padding: 0 9px; min-height: 22px; }
1319.attach-add:hover { border-color: var(--accent); color: var(--accent-text); }
1320/* And a `+` that widens what a daimon may write wears the accent every other
1321 granting control in this app wears, so the two are told apart without reading
1322 either one. */
1323.attach-add.grants { color: var(--accent-text); border-color: var(--accent);
1324 background: var(--accent-soft); }
1325.attach-add.grants:hover { background: var(--accent); color: var(--on-fill, #fff); }
1326
1327/* The legend at the head of the picker, where a tick on a folder and a tick on
1328 a file mean two different things and no row has room to say which. */
1329.attach-pick-note { padding: 6px 2px 8px; line-height: 1.45;
1330 font-size: var(--fs-3xs-2xs); color: var(--accent-text); }
1331
1332/* The tiles themselves, and the only thing in either footer that scrolls. The
1333 cap is `--attach-cap` tiles, which is six and is measured -- variables.css
1334 says how it was arrived at. `scrollbar-gutter: stable` keeps the width from
1335 twitching as the last tile is taken off, and the track is painted rather
1336 than left as an overlay that fades: the contract asks for a footer that
1337 LOOKS like it scrolls. */
1338.attach-body { max-height: calc(var(--attach-cap) * var(--attach-row-h) + var(--attach-peek));
1339 overflow-y: auto; scrollbar-gutter: stable; scrollbar-width: thin; }
1340.attach-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
1341.attach-body::-webkit-scrollbar { width: 10px; }
1342.attach-body::-webkit-scrollbar-track { background: var(--bg-secondary); border-radius: 5px; }
1343.attach-body::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 5px;
1344 border: 2px solid var(--bg-secondary); }
1345
1346/* The other view: rows of sizeable icons (§5), which the note is explicit are
1347 NOT button-sized. An 88x84 cell against the 26px buttons beside it -- big
1348 enough to hit without aiming, and to read the name off at a glance. */
1349.attach-body.icons { display: flex; flex-wrap: wrap; align-content: flex-start;
1350 gap: 8px; padding: 8px; }
1351.attach-icon { position: relative; width: var(--attach-icon-w); height: var(--attach-icon-h);
1352 box-sizing: border-box; display: flex; flex-direction: column; align-items: center;
1353 gap: 2px; padding: 6px 4px 4px; border: 1px solid var(--border); border-radius: var(--radius);
1354 background: var(--bg-secondary); }
1355.attach-icon .arte-kind { min-width: 0; font-size: 26px; line-height: 1.05; }
1356.attach-icon .arte-open { flex: none; width: 100%; text-align: center; white-space: normal;
1357 overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
1358 font-size: var(--fs-3xs-2xs); line-height: 1.25; word-break: break-word; }
1359.attach-icon .attach-state { margin-top: auto; }
1360/* The reason a shut tile gives is a sentence, and a sentence does not fit an
1361 88px cell. On an icon it is the hover title and the cell says `?`; the stack
1362 view is where it is written out, and the toggle is one press away. */
1363.attach-icon .arte-why { position: absolute; top: 2px; left: 4px; padding: 0;
1364 font-size: var(--fs-3xs-2xs); color: var(--text-muted); overflow: hidden;
1365 width: 12px; height: 12px; white-space: nowrap; text-indent: -999px; }
1366.attach-icon .arte-why::after { content: '?'; position: absolute; left: 0; top: 0; text-indent: 0; }
1367.attach-icon.shut { border-style: dashed; }
1368/* The refer arrow and the × sit in the corner of an icon rather than in a row
1369 of their own: a cell this size has no row to spare. */
1370.attach-icon .arte-use, .attach-icon .arte-drop { position: absolute; top: 0; padding: 1px 4px;
1371 font-size: var(--fs-xs); opacity: 0; }
1372.attach-icon .arte-use { right: 16px; }
1373.attach-icon .arte-drop { right: 0; }
1374.attach-icon:hover .arte-use, .attach-icon:hover .arte-drop,
1375.attach-icon:focus-within .arte-use, .attach-icon:focus-within .arte-drop { opacity: 1; }
1376
1377/* The `+` picker: the workspace's own tree, in the app's own dialog, with a
1378 tick per row. Nothing here is a new modal -- see `dialog`'s `pick` kind. */
1379.attach-pick { max-height: 46vh; overflow-y: auto; margin: 8px 0 0;
1380 border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-tertiary); }
1381.attach-pick-path { border-bottom: none; padding: 4px 2px; }
1382.attach-pick-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px;
1383 border-bottom: 1px solid var(--border); }
1384.attach-pick-row:last-child { border-bottom: none; }
1385.attach-pick-tick { flex: none; width: 16px; height: 16px; cursor: pointer; accent-color: var(--accent); }
1386.attach-pick-tick:disabled { cursor: default; }
1387.attach-pick-name { flex: 1; min-width: 0; text-align: left; background: none; border: none;
1388 padding: 0; color: var(--text-primary); font-size: var(--fs-xs-sm);
1389 font-family: var(--font-mono); cursor: pointer;
1390 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1391.attach-pick-name.dir:hover { color: var(--accent-text); }
1392
1393/* Links: how this Diamond stands to the other Diamonds. The artefact strip
1394 above holds the links pointing at a file, a page or a chat; these are the
1395 ones pointing at another Diamond, which is a different question. Unlike the
1396 strip this header never hides, because it is also the only way in to making
1397 a link, and a control that appears once there is already one of the thing it
1398 makes is a control nobody finds. */
1399.link-sec { margin: 2px 0 6px; }
1400.link-strip { display: block; width: 100%; text-align: left; background: none; border: none;
1401 color: var(--text-muted); font-size: var(--fs-xs); font-family: var(--font-mono);
1402 padding: 4px 2px; cursor: pointer; }
1403.link-strip:hover { color: var(--text-secondary); }
1404.link-body { border: 1px solid var(--border); border-radius: var(--radius);
1405 background: var(--bg-tertiary); padding: 6px; display: flex; flex-direction: column; gap: 6px; }
1406.link-list { max-height: 30vh; overflow-y: auto; }
1407.link-row { border-bottom: 1px solid var(--border); padding: 3px 2px; }
1408.link-row:last-child { border-bottom: none; }
1409/* The direction is what a row is FOR, so it is said twice over: by the order of
1410 the two ends in the phrase, and by which side the coloured edge sits on. */
1411.link-row-out { border-left: 2px solid var(--accent); padding-left: 6px; }
1412.link-row-in { border-right: 2px solid var(--border-2); padding-right: 6px; }
1413.link-line { display: flex; align-items: center; gap: 6px; }
1414.link-phrase { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
1415.link-self { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted);
1416 text-transform: uppercase; letter-spacing: .04em; }
1417.link-arrow { color: var(--text-muted); font-size: var(--fs-xs); }
1418.link-rel { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-secondary);
1419 background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 20px;
1420 padding: 1px 8px; white-space: nowrap; }
1421.link-other { background: none; border: none; padding: 2px 0; cursor: pointer;
1422 color: var(--text-primary); font-size: var(--fs-xs-sm); text-align: left;
1423 max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1424.link-other:hover { color: var(--accent-text); }
1425.link-acts { flex: none; display: flex; gap: 2px; }
1426.link-note-btn, .link-drop { background: none; border: none; cursor: pointer;
1427 color: var(--text-muted); font-size: var(--fs-sm); line-height: 1; padding: 2px 5px;
1428 min-width: 26px; min-height: 26px; }
1429.link-note-btn:hover, .link-note-btn.on { color: var(--accent); }
1430.link-drop:hover { color: var(--danger, #e5534b); }
1431.link-note { font-size: var(--fs-xs); color: var(--text-secondary); line-height: 1.5;
1432 padding: 2px 2px 4px 6px; }
1433.link-none { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; padding: 4px 2px; }
1434.link-add { align-self: flex-start; font-size: var(--fs-xs); padding: 6px 11px; min-height: 34px;
1435 background: var(--bg-secondary); color: var(--text-secondary);
1436 border: 1px solid var(--border-strong); border-radius: var(--radius-sm); cursor: pointer; }
1437.link-add:hover { background: var(--bg-hover); color: var(--text-primary); }
1438
1439/* The add form. Every value it holds lives in JS rather than in these fields,
1440 so a repaint under a half-filled form restores it instead of emptying it. */
1441.link-form { display: flex; flex-direction: column; gap: 8px;
1442 border-top: 1px solid var(--border); padding-top: 8px; }
1443.link-field { display: flex; flex-direction: column; gap: 4px; }
1444.link-label { font-size: var(--fs-2xs); font-weight: 600; letter-spacing: .04em;
1445 text-transform: uppercase; color: var(--text-muted); }
1446.link-pick, .link-rel-input, .link-note-input { background: var(--bg-secondary);
1447 border: 1px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary);
1448 font-size: var(--fs-sm); padding: 6px 8px; min-height: 34px; width: 100%; }
1449.link-pick:focus, .link-rel-input:focus, .link-note-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1450.link-pick::placeholder, .link-rel-input::placeholder, .link-note-input::placeholder { color: var(--text-muted); }
1451.link-picks { display: flex; flex-wrap: wrap; gap: 4px; }
1452.link-pick-hit { font-size: var(--fs-xs); padding: 6px 10px; min-height: 34px; cursor: pointer;
1453 background: var(--bg-secondary); color: var(--text-primary);
1454 border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
1455 max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1456.link-pick-hit:hover { border-color: var(--accent); background: var(--bg-hover); }
1457.link-chosen { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
1458.link-chosen-name { font-size: var(--fs-sm); color: var(--text-primary); }
1459.link-change { font-size: var(--fs-xs); padding: 6px 10px; min-height: 34px; cursor: pointer;
1460 background: none; color: var(--text-secondary); border: 1px solid var(--border-strong);
1461 border-radius: var(--radius-sm); }
1462.link-change:hover { color: var(--text-primary); }
1463.link-rel-sug { display: flex; flex-wrap: wrap; gap: 4px; }
1464.link-sug { font-family: var(--font-mono); font-size: var(--fs-2xs); padding: 5px 9px;
1465 min-height: 28px; cursor: pointer; background: none; color: var(--text-muted);
1466 border: 1px dashed var(--border); border-radius: 20px; }
1467.link-sug:hover { color: var(--text-primary); border-style: solid; border-color: var(--accent); }
1468.link-says { font-size: var(--fs-2xs); color: var(--text-muted); line-height: 1.5; }
1469.link-err { font-size: var(--fs-xs); color: var(--danger, #e5534b); }
1470.link-err:empty { display: none; }
1471.link-form-acts { display: flex; gap: 6px; }
1472.link-save, .link-cancel { font-size: var(--fs-xs); padding: 7px 13px; min-height: 34px;
1473 border-radius: var(--radius-sm); cursor: pointer; }
1474.link-save { background: var(--accent); color: var(--on-fill, #fff); border: 1px solid var(--accent); }
1475.link-save:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
1476.link-save:disabled { opacity: .5; cursor: default; }
1477.link-cancel { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-strong); }
1478.link-cancel:hover { background: var(--bg-hover); color: var(--text-primary); }
1479
1480/* At a phone's width every target is a thumb's target, and the phrase wraps
1481 rather than pushing a row's controls off the edge. */
1482@media (max-width: 420px) {
1483 .link-note-btn, .link-drop { min-width: 34px; min-height: 34px; font-size: var(--fs-base); }
1484 .link-sug { min-height: 34px; padding: 7px 11px; }
1485 .link-body { padding: 6px 5px; }
1486}
1487
1488/* Fold-a-delta control: enter a delta, propose a fold (writes nothing). */
1489.crystal-status { font-size: var(--fs-xs); color: var(--text-muted); font-family: var(--font-mono); min-height: 14px; }
1490/* The waiter on the crystal face. The dots are `.chat-spinner-dot`, the same
1491 class the thread's spinner uses, so the two faces cannot drift and the
1492 reduced-motion rule below already covers these. Only the box around them is
1493 new: inline-flex so the dots sit ON the status line beside its words rather
1494 than above them, and a right margin because `gap` would set the space before
1495 the sentence to the (much tighter) space between the dots. */
1496.crystal-dots { display: inline-flex; align-items: center; gap: 3px; margin-right: 6px; vertical-align: middle; }
1497.crystal-dots .chat-spinner-dot { width: 5px; height: 5px; }
1498/* --panel-2 and --text were never declared, so this box fell back to a white
1499 wash and a white hover: invisible surface on the light themes, invisible × on
1500 both of them. Every colour here is a real token now. */
1501.crystal-reply { position: relative; margin: 6px 0; padding: 8px 26px 8px 10px; border-radius: 8px;
1502 background: var(--bg-tertiary); border: 1px solid var(--border);
1503 font-size: var(--fs-sm); line-height: 1.45; max-height: 40vh; overflow-y: auto; }
1504.crystal-reply-body > :first-child { margin-top: 0; }
1505.crystal-reply-body > :last-child { margin-bottom: 0; }
1506.crystal-reply-x { position: absolute; top: 4px; right: 6px; border: none; background: none;
1507 color: var(--text-muted); cursor: pointer; font-size: var(--fs-xl); line-height: 1; padding: 2px 4px; }
1508.crystal-reply-x:hover { color: var(--text-primary); }
1509
1510/* The reducer is a paid round trip and the slowest thing in the app, so the
1511 crystal shows the same three dots the thread does while it runs. */
1512.crystal-spinner { padding: 8px 2px; }
1513
1514/* A fold proposed on a Diamond that is not the one on screen. It sits with the
1515 Diamond's other plain facts in the rail, and is the only thing that says there
1516 is a proposal waiting to be accepted or rejected. */
1517.diamond-pending { font-size: var(--fs-2xs); color: var(--accent-text); font-weight: 600; }
1518
1519/* Fold diff — added / removed lines, all text escaped. */
1520.diff-head { font-size: var(--fs-xs); color: var(--text-secondary); margin-bottom: 10px; }
1521/* A key the proposal would drop. Warning colour, not error: this is a thing the user may
1522 well want, and the point is that they see it rather than that they are stopped. */
1523.diff-lost {
1524 font-size: var(--fs-xs);
1525 color: var(--warn);
1526 border-left: 3px solid var(--warn);
1527 padding: 6px 10px;
1528 margin-bottom: 10px;
1529 background: var(--warn-bg);
1530 border-radius: var(--radius-sm);
1531}
1532.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; }
1533.diff-line { display: block; white-space: pre-wrap; word-break: break-word; padding: 1px 8px; }
1534.diff-line .sign { display: inline-block; width: 1.3em; color: var(--text-muted); user-select: none; }
1535.diff-line.add { background: rgba(102, 185, 140, 0.16); }
1536.diff-line.del { background: rgba(224, 122, 103, 0.16); }
1537.diff-line.add .sign { color: var(--ok); }
1538.diff-line.del .sign { color: var(--danger); }
1539.diff-actions { display: flex; gap: 8px; }
1540.diff-accept { background: var(--ok); color: var(--on-fill, #fff); border: none; border-radius: var(--radius); padding: 9px 16px; font-size: var(--fs-base); }
1541.diff-accept:hover { filter: brightness(1.08); }
1542.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); }
1543.diff-reject:hover { border-color: var(--danger); color: var(--danger); }
1544
1545/* Empty state */
1546.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; }
1547.empty-state h2 { margin: 0; color: var(--text-primary); font-size: var(--fs-3xl); }
1548.empty-state p { margin: 0; max-width: 360px; }
1549/* The empty chat opens straight on its one action, with the welcome copy removed. */
1550.empty-state.bare { gap: 0; }
1551.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); }
1552.empty-state.bare .empty-new-session { margin-top: 0; }
1553.empty-new-session:hover { background: var(--accent-hover); }
1554
1555/* The turn indicator: three dots and a line saying what the turn is doing.
1556 It is up for the whole turn, so it sits at the foot of the thread and the
1557 caption is what stops it being four minutes of unchanging nothing. */
1558.chat-spinner { display: flex; align-items: center; gap: 4px; padding: 8px 14px; }
1559/* The caption. Muted and small: it is a status line beside the answer, not part
1560 of it, and it must not read as something the model said. The left margin is
1561 the gap between the dots and the words -- `gap` alone would set it to the
1562 spacing BETWEEN the dots, which is far too tight for a sentence. */
1563.chat-spinner-say { margin-left: 6px; font-size: var(--fs-xs); color: var(--text-secondary); }
1564.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; }
1565.chat-spinner-dot:nth-child(1) { animation-delay: -0.32s; }
1566.chat-spinner-dot:nth-child(2) { animation-delay: -0.16s; }
1567@keyframes spinner-bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
1568/* A reader who has asked for less motion still needs to know the model is
1569 working, so the dots stay -- they stop moving. `animation: none` alone would
1570 not do: the keyframe rests them at scale(0), so switching it off mid-cycle
1571 leaves three invisible dots and a spinner that says nothing. */
1572@media (prefers-reduced-motion: reduce) {
1573 .chat-spinner-dot { animation: none; transform: scale(1); opacity: 0.5; }
1574 .chat-spinner-dot:nth-child(2) { opacity: 0.75; }
1575 .chat-spinner-dot:nth-child(3) { opacity: 1; }
1576}
1577
1578/* Inline tool-call blocks */
1579.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); }
1580.tool-head { padding: 7px 12px; cursor: pointer; font-weight: 600; color: var(--accent-text); user-select: none; display: flex; align-items: center; gap: 6px; }
1581.tool-head::before { content: '▾'; font-size: var(--fs-3xs); color: var(--text-muted); transition: transform 0.15s; }
1582.tool-block.collapsed .tool-head::before { transform: rotate(-90deg); }
1583.tool-head:hover { background: var(--bg-hover); }
1584.tool-block.running .tool-head::after { content: '…'; color: var(--text-secondary); }
1585.tool-block.failed { border-color: var(--danger); }
1586.tool-block.failed .tool-result { color: var(--danger); }
1587.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; }
1588.tool-args { color: var(--text-secondary); }
1589.tool-result { color: var(--text-primary); }
1590/* Why a command in this block could not reach anything. NOT a failure -- the fence
1591 did what the user asked -- so the warning colour the refusals use and never the
1592 danger one, and prose rather than the monospace the tool's own output is in. */
1593.tool-nonet { padding: 8px 12px; border-top: 1px solid var(--border); color: var(--warn); font-size: var(--fs-xs); }
1594/* Collapsed by default: hide the (often long) args JSON, keep the result. */
1595.tool-block.collapsed .tool-args { display: none !important; }
1596.chat-output.hide-tools .tool-block { display: none; }
1597/* The model thinking out loud on its way to a tool call, and IT DOES NOT HIDE WITH THE
1598 STEPS. It did until 2026-08-28, on the reasoning that a reader who turned the working
1599 off meant this too, and what that actually did was take the model's own sentences off
1600 the screen: a turn of twenty calls kept nothing but its last paragraph. The switch is
1601 called Steps and it hides the steps. See `demoteToWorking` in www/js/daimond.js. */
1602.chat-msg-working { margin: 4px 0 4px 2px; }
1603.chat-working-line {
1604 color: var(--text-muted);
1605 font-size: var(--fs-sm);
1606 font-style: italic;
1607 opacity: 0.72;
1608}
1609/* How much of the model's working went behind the control. Quieter than the summary
1610 it sits after, because it is a measurement and not a sentence -- but it is THERE,
1611 which is the point: a summary alone reads as text that went away rather than as a
1612 control that opens on it. See `demoteToWorking` in www/js/daimond.js. */
1613.chat-working-more {
1614 margin-left: 6px;
1615 color: var(--text-muted);
1616 font-size: var(--fs-xs);
1617 font-variant-numeric: tabular-nums;
1618 white-space: nowrap;
1619}
1620
1621/* Context-window meter (session boxes + agent tiles) */
1622.ctx-meter { display: flex; align-items: center; gap: 6px; margin-top: 5px; }
1623.ctx-bar { flex: 1; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
1624.ctx-fill { height: 100%; background: var(--accent); transition: width 0.3s ease; }
1625.ctx-fill.high { background: var(--danger); }
1626.ctx-meter-label { font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-secondary); white-space: nowrap; }
1627
1628/* ── Agents panel ────────────────────────────────────────────── */
1629.agents-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
1630/* 10px, which is the inset every dock panel's first body line now takes. The
1631 four disagreed — Agents 6, Email 10, Spending 14, Workspace 11 — and four
1632 panels side by side in one dock with four different left edges is the kind of
1633 thing you see without being able to name. 10 reads well against the 4px
1634 header without moving the header. Email already had it; mail.css, files.css
1635 and spend.css carry the same number. */
1636.agents-empty { font-size: var(--fs-xs); color: var(--text-muted); padding: 2px 10px; }
1637.agent-find { padding-top: 2px; padding-bottom: 2px; }
1638/* Chips on an agent tile: parent Diamond, its inherited tags, the model. */
1639.acard .achips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin: 4px 0 2px; }
1640.acard .diamond-chip { font-weight: 500; }
1641.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); }
1642/* A worker routed by modality. Marked rather than recoloured: which model a run
1643 is on is the fact, and how it came to be on it is a note about that fact. */
1644.acard .achip-model.achip-vision { border-style: dashed; }
1645/* A worker that was MOVED names both models in the chip itself -- the one it left
1646 and the one it went to. Wider than an ordinary model chip because it carries two
1647 names, and it must not ellipse away the half that says anything moved. */
1648.acard .achip-model.achip-rerouted { max-width: 232px; border-style: dashed; border-color: var(--accent); color: var(--text-secondary); }
1649/* What the app did to a picture, in the app's own voice, under the chips. Not a
1650 title: a tooltip is not disclosure, and a tooltip was all this said on the night
1651 it sent a whole run to a model that could not see. */
1652.acard .anote { font-size: var(--fs-2xs); line-height: 1.35; margin: 3px 0 0; color: var(--text-muted); }
1653.acard .anote.avision { color: var(--text-secondary); }
1654
1655.acard { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 10px; padding: 8px 9px; flex: none; }
1656.acard.running { border-color: var(--border-2); }
1657.acard.done { border-color: var(--ok); }
1658.acard.error { border-color: var(--danger); }
1659.acard .ah { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 3px; }
1660.acard .an { font-size: var(--fs-xs); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1661.pill { font-family: var(--font-mono); font-size: var(--fs-3xs); padding: 1px 8px; border-radius: 20px; white-space: nowrap; flex: none; }
1662.pill.run { color: var(--accent-text); background: var(--accent-soft); }
1663.pill.ok { color: var(--ok); background: var(--ok-bg); }
1664.pill.err { color: var(--danger); background: var(--accent-soft); }
1665.acard .arow { display: flex; justify-content: space-between; gap: 6px; font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); }
1666.atools { margin-top: 5px; display: flex; flex-direction: column; gap: 2px; }
1667.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; }
1668/* Only a row that can actually take you somewhere offers the affordance. */
1669.atool.linked { cursor: pointer; }
1670.atool.linked:hover { background: var(--bg-hover); color: var(--text-primary); }
1671.atool.failed { color: var(--danger); }
1672.atool .cross { color: var(--danger); flex: none; }
1673.atool .tick { color: var(--ok); flex: none; }
1674.atool .live { color: var(--accent); flex: none; animation: atool-pulse 1.2s infinite ease-in-out; }
1675@keyframes atool-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
1676/* The running tool keeps its accent colour instead of pulsing: the state was
1677 carried by the movement, so something has to carry it once the movement is
1678 gone. */
1679@media (prefers-reduced-motion: reduce) {
1680 .atool .live { animation: none; opacity: 1; font-weight: 600; }
1681}
1682
1683/* ── Mobile nav (hidden on desktop; see responsive.css) ──────── */
1684.mnav { display: none; }
1685
1686/* ── The closer, and the row it sits in ──────────────────────────
1687 Built by www/js/closer.js, worn by every dialog, popover, drawer and sheet.
1688 One set of rules, because five spellings of one control is how four of them
1689 ended up under the thumb's floor.
1690
1691 28px here and 44px where the pointer is coarse (see responsive.css). The
1692 drawn cross keeps `.ic`'s 16px in both: the TARGET is a thumb's and the INK
1693 is an eye's, and growing the ink with the box would put a great black X in
1694 the corner of every card on a phone.
1695
1696 In flow, never absolute. `.modal-card` and `.pop` are both `overflow-y:
1697 auto`, so an absolutely positioned closer would resolve against the initial
1698 containing block -- 4px from the top of the VIEWPORT rather than of the card
1699 -- and would stay there while the content scrolled underneath it. */
1700.ui-close {
1701 flex: none;
1702 display: flex; align-items: center; justify-content: center;
1703 width: 28px; height: 28px; padding: 0;
1704 border: none; border-radius: var(--radius-sm); background: none;
1705 color: var(--text-muted);
1706 cursor: pointer;
1707}
1708.ui-close:hover { color: var(--text-primary); background: var(--bg-hover); }
1709.ui-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
1710/* The title row: what the surface is, and the way out beside it. `flex-start`
1711 rather than centre, because a long name wraps and would otherwise drag the
1712 cross down the side of it. */
1713.ui-head {
1714 display: flex; align-items: flex-start; justify-content: space-between;
1715 gap: 12px; margin-bottom: 10px;
1716}
1717/* The row carries the space the heading used to carry itself; left on, the
1718 heading's margin would open a gap under the cross as well as under the
1719 words. */
1720.ui-head > :first-child { flex: 1 1 auto; min-width: 0; }
1721.ui-head h2 { margin-bottom: 0; overflow-wrap: anywhere; }
1722/* A dialog's own heading keeps its type; a popover's title is a label, so it
1723 is set like one. Not `.pop-head`, which marks a SECTION inside the popover:
1724 a second one of those at the top would read as an empty first section. */
1725.ui-head-title {
1726 font-size: var(--fs-sm); font-weight: 600;
1727 color: var(--text-secondary);
1728 overflow-wrap: anywhere;
1729}
1730.pop > .ui-head { margin-bottom: 4px; padding: 2px 0 2px 6px; }
1731
1732/* Modal */
1733.modal { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
1734.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; }
1735.modal-card h2 { font-size: var(--fs-2xl); margin-bottom: 16px; }
1736/* The 8px that puts every Admin view on one left edge. `#admin-scroll` gives
1737 8px to all of them; Home's rows add another 8 of their own (`.admin-item`,
1738 `.admin-sec`) and the form views added none, so switching from Home to Models
1739 or Credits shifted every word 8px left. Measured off the ink: Home 16, Models
1740 8, Credits 8. */
1741.settings-section { margin-bottom: 20px; padding: 0 8px; }
1742.settings-section h3 { font-size: var(--fs-lg); margin-bottom: 12px; color: var(--text-secondary); }
1743.settings-section form { display: flex; flex-direction: column; gap: 10px; }
1744.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); }
1745.settings-section input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1746.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); }
1747/* ── Why a pulldown says `:focus-visible` and a text field does not ──
1748 The accent ring means "the keyboard is here". A mouse click that paints it
1749 says something untrue, which is why the forty-odd button rules in this app
1750 are `:focus-visible` -- and this rule, alone among the pulldowns, was not.
1751
1752 The two selectors are NOT interchangeable, and which controls they differ on
1753 was measured rather than assumed (a real `page.mouse.click`, the pointer then
1754 parked off the control so `:hover` could not be mistaken for a ring):
1755
1756 control Chromium :focus-visible Firefox :focus-visible
1757 <select> true FALSE
1758 <input type=text> true true
1759 <button> false false
1760 [tabindex] div false false
1761
1762 So on a pulldown the change is real -- Firefox stops ringing a clicked
1763 select, which is what Firefox does for every other select on the web -- while
1764 on a TEXT FIELD it paints identically in both engines, because a control that
1765 takes typing always matches `:focus-visible` by design. The app's text-field
1766 `:focus` rules are therefore left exactly as they are: changing them moves no
1767 pixel in any engine, and the caret is what says typing will land there. */
1768.settings-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1769.settings-select option:disabled { color: var(--text-muted); }
1770.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); }
1771.settings-section button:hover { background: var(--accent-hover); }
1772.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%; }
1773.modal-close:hover { background: var(--bg-hover); color: var(--text-primary); }
1774
1775/* BYOK settings helpers (browser-only Daimond). */
1776.cfg-check { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); color: var(--text-secondary); }
1777.cfg-check input { width: auto; }
1778.cfg-hint { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 10px; line-height: 1.5; }
1779.cfg-credits-hint { color: var(--text-secondary); }
1780.cfg-lead { font-size: var(--fs-sm); color: var(--text-secondary); margin: -4px 0 4px; line-height: 1.5; }
1781.byok-form { display: flex; flex-direction: column; gap: 10px; }
1782.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); }
1783.byok-form input[type="text"]:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1784.byok-form input::placeholder { color: var(--text-muted); }
1785#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; }
1786#byok-save:hover { filter: brightness(1.08); }
1787/* The default suits a plain block host (the admin card's built forms). The
1788 BYOK form spaces rows with a flex gap instead, so it compensates below --
1789 a negative default here put every OTHER form's label flush on its box. */
1790.cfg-fieldlabel { display: block; font-size: var(--fs-xs); color: var(--text-secondary); margin: 12px 0 6px; }
1791.cfg-fieldlabel:first-child { margin-top: 0; }
1792/* A CHILD of the form, not a descendant. The -4px only makes sense where the
1793 form's own 10px gap is above the label; #cfg-url-row is a plain div holding
1794 the Base URL pair, no gap reaches inside it, and the compensation there put
1795 the label 4px down onto its own box. As the row's first child the label
1796 already takes margin-top:0 from the rule above, so it needs nothing else. */
1797.byok-form > .cfg-fieldlabel { margin: 0 0 -4px; }
1798/* Same trap on the other side of the field, and the one the label's fix missed.
1799 Three pulldowns are inserted into the Models section as plain blocks (steps
1800 per turn, longest reply, crystal size) and the Push form is a block too, so
1801 the note rode up into the box it explains. 6px in both hosts: the note binds
1802 to the field above it, and the 12px before the next label keeps it there. */
1803.cfg-fieldnote { font-size: var(--fs-xs); color: var(--text-muted); margin: 6px 0 0; line-height: 1.5; }
1804.byok-form .cfg-fieldnote { margin: -4px 0 0; }
1805/* A heading over a PAIR of rows that are one setting to a person -- the crystal's
1806 data limit and its page limit. Presented apart they read as two unrelated
1807 numbers. Heavier than a field label and lighter than the section title, with
1808 a rule under it so the two rows beneath read as belonging to it. */
1809.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); }
1810.cfg-fieldhead + .cfg-fieldlabel { margin-top: 8px; }
1811.cfg-advanced summary { font-size: var(--fs-sm); color: var(--text-secondary); cursor: pointer; user-select: none; padding: 2px 0; }
1812.cfg-advanced[open] { display: flex; flex-direction: column; gap: 10px; }
1813
1814/* ── Search: which service the agent searches with ──────────────
1815 A row is a label, its field and the notes that explain it, kept together so
1816 the notes cannot drift onto the field below on a narrow rail. Same weights
1817 as the model picker directly above it, because it is the same kind of
1818 decision and a second visual language for it would say otherwise. */
1819.search-engine-row { display: flex; flex-direction: column; margin-top: 12px; }
1820.search-engine-row:first-of-type { margin-top: 0; }
1821.search-engine-row input[type="text"] {
1822 width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-strong);
1823 border-radius: var(--radius-sm); color: var(--text-primary);
1824 padding: 9px 12px; font-size: var(--fs-base);
1825}
1826.search-engine-row input[type="text"]:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1827.search-engine-row input::placeholder { color: var(--text-muted); }
1828.search-engine-note { font-size: var(--fs-xs); color: var(--text-muted); margin: 6px 0 0; line-height: 1.5; }
1829.search-engine-note a { color: var(--accent-text); }
1830/* AMBER, not red. "You have not added a key yet" is a state of the setting and
1831 a thing to do about it; drawing it as an error would report a fault at
1832 somebody who has done nothing wrong -- the same rule the credits row follows
1833 when the balance runs out. */
1834.search-engine-note[data-warn] { color: var(--warn); }
1835
1836/* `root/web`'s control, in the Web panel header. Ruled off from the page
1837 buttons beside it because it is a different KIND of control: back, reload
1838 and pop act on the page, and this decides whether the panel may spend at
1839 all. Without the rule it reads as a fourth page button with a lamp on it. */
1840.web .chead-right .pptw { padding-right: 8px; border-right: 1px solid var(--border); }
1841
1842/* ── Chat tiles: pending (pick model → Start) + active (meter + Fold) ─── */
1843/* A chat's label is DERIVED and no longer an editable field: it is a button
1844 whose only act is opening the chat. It keeps the field's metrics so the rail
1845 does not reflow, and loses the field's hover border, which used to be an
1846 invitation to type. `text-align: left` because a button centres by default
1847 and every other line on the tile is ranged left. */
1848.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; }
1849.tile-label:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
1850.session-box.active .tile-label { color: var(--accent-text); }
1851/* The identity itself: a name if one was set, otherwise the relative time. */
1852.tile-when { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1853/* The clock beside a name that WAS set. Quieter than the name and never
1854 shrinking below its own content, so a long name is what gets clipped -- the
1855 time is the one thing on this row that is the same width for every tile. */
1856.tile-when-sub { flex: none; font-size: var(--fs-2xs); font-weight: 400; color: var(--text-muted); }
1857.session-box.active .tile-when-sub { color: inherit; opacity: 0.75; }
1858/* The first thing said in the chat, dimmed, on its own line. ONE line, clipped
1859 with an ellipsis: two would double the height of every tile in the rail, and
1860 the whole line is on the `title` for anyone who wants it. */
1861.tile-preview { margin-top: 3px; font-size: var(--fs-2xs); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1862.session-box.active .tile-preview { color: inherit; opacity: 0.7; }
1863/* NOT a preview: a chat whose transcript is on another device and has not
1864 arrived. It sits in the preview's place and inherited the preview's paint, so
1865 the one tile a reader most needs told apart from an empty chat was drawn
1866 exactly like the first line of a full one. Italic and one notch quieter, and
1867 the app's own "waiting" colour rather than a fourth grey — it is a state, not
1868 a quotation, and nothing about it is the user's words. */
1869.tile-waiting { font-style: italic; opacity: 0.85; }
1870.session-box.active .tile-waiting { color: inherit; opacity: 0.6; }
1871/* The day heading above a run of tiles. Furniture, not a control: it is the
1872 coarse half of a chat's identity, so it is set small and quiet and the tile
1873 underneath carries the part that varies. */
1874.rail-day { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding: 8px 2px 3px; }
1875.rail-day:first-child { padding-top: 2px; }
1876/* The rename field, in the cog dialog and nowhere else. */
1877.tile-dlg-name { padding: 0 2px 4px; }
1878.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; }
1879.tile-dlg-name-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1880.session-box.pending { border-style: dashed; }
1881
1882.tile-pending { display: flex; gap: 6px; margin-top: 7px; }
1883/* The name of a control on a tile -- "Workers", beside its pulldown. It wore
1884 `.tile-model-chip` and so read as a VALUE: that pill is the badge a model's
1885 name wears, and a label in one puts two pills on a row where only the right
1886 one is data. The pill comes off; the word stays.
1887 Sized for a rail tile and not for the dialog: `.tile-dlg-label` holds a fixed
1888 116px column because three rows there have to line up, and a 260px tile has
1889 no width to spare for a column of one. `flex: none` and no wrap, so the label
1890 takes exactly its own width and the pulldown keeps the rest.
1891 11px in the UI font rather than the chip's 10.5px monospace -- a word set in
1892 a text face reads larger at the same measure than one set in mono, and this
1893 is the smallest step at which a label is still comfortable. */
1894.tile-row-label {
1895 flex: none; align-self: center;
1896 font-size: var(--fs-2xs); color: var(--text-secondary);
1897 white-space: nowrap; cursor: pointer;
1898}
1899/* Asymmetric on purpose: the browser draws a select's arrow inside the padding
1900 box, so a symmetric 6px leaves it all but touching the border. The arrow side
1901 gets the air; the text side does not need it. This is the base every pulldown
1902 in the app inherits, the dialog's larger one included. */
1903.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; }
1904/* A pulldown, so `:focus-visible` -- see the note at `.settings-select`. */
1905.tile-model:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
1906.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; }
1907.tile-start:hover { background: var(--accent-hover); }
1908
1909.tile-active { display: flex; flex-direction: column; gap: 5px; margin-top: 7px; }
1910.tile-active-top { display: flex; align-items: center; gap: 6px; }
1911.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; }
1912/* Fold and Keep are pushed to the far end together, so the model chip keeps the
1913 near edge and the two acts sit as a pair. It was `.tile-fold` alone that
1914 carried the margin; with a second button beside it the rule has to be about
1915 whichever comes first, or Keep would be shoved away from Fold. */
1916.tile-active-top .tile-fold, .tile-active-top .tile-keep:first-of-type { margin-left: auto; }
1917.tile-active-top .tile-fold ~ .tile-keep { margin-left: 0; }
1918.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); }
1919.tile-ctx { display: flex; align-items: center; gap: 4px; }
1920/* `position: relative` so the fold mark can be placed along it, and the overflow
1921 clip that keeps the fill inside the rounded ends keeps the mark inside too. */
1922.tile-ctx-bar { position: relative; width: 32px; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
1923.tile-ctx-fill { display: block; height: 100%; background: var(--accent); }
1924.tile-ctx-fill.high { background: var(--danger); }
1925/* Where the conversation folds. A hairline rather than a colour change, so the
1926 bar still reads as one quantity: the mark says where the threshold is, and
1927 the fill going `.high` says it has been passed. */
1928.tile-ctx-fold {
1929 position: absolute; top: 0; bottom: 0; width: 1px;
1930 background: var(--text-muted); opacity: 0.9;
1931}
1932.tile-ctx-pct { color: var(--text-secondary); }
1933.tile-tok { white-space: nowrap; }
1934.tile-cost { color: var(--text-secondary); white-space: nowrap; }
1935.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; }
1936.tile-fold:hover { border-color: var(--accent); color: var(--accent-text); }
1937/* Keep as a Diamond. It wears the SAME shape as Fold because they sit side by
1938 side and a louder one would read as the recommended act -- which it is not;
1939 they answer different questions. What separates them is the word. */
1940.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; }
1941.tile-keep:hover { border-color: var(--accent); color: var(--accent-text); }
1942
1943/* Fold-into picker (a small anchored menu of Diamonds). */
1944.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; }
1945.fold-menu-head { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); padding: 4px 8px 2px; }
1946.fold-menu-empty { font-size: var(--fs-xs); color: var(--text-muted); padding: 2px 8px; }
1947.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; }
1948.fold-menu-item:hover { background: var(--bg-hover); }
1949.fold-menu-item.new { color: var(--accent-text); border-top: 1px solid var(--border); margin-top: 2px; }
1950
1951/* The Chats section's own overflow -- "Delete all chats" and, later, whatever
1952 else is about the whole list rather than one tile. Same floating-popover
1953 shape as `.fold-menu` above, its own class only because it hangs off a
1954 different anchor and is not part of the fold feature. */
1955.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; }
1956.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; }
1957.railhead-menu-item:hover { background: var(--bg-hover); }
1958.railhead-menu-item.danger { color: var(--danger); }
1959.railhead-menu-item.danger:hover { background: var(--danger); color: var(--on-fill, #fff); }
1960.railhead-menu-item:disabled { color: var(--text-muted); cursor: default; }
1961.railhead-menu-item:disabled:hover { background: none; }
1962
1963/* Global spend readout (session / week / month). */
1964.spend-row { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 2px; border-top: 1px solid var(--border); }
1965.spend-cell { flex: 1; display: flex; flex-direction: column; gap: 1px; align-items: center; padding: 2px 4px; border-radius: var(--radius-sm); }
1966.spend-label { font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
1967.spend-amt { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); }
1968/* The spend governor's quiet "faster than usual" note: a full-width row
1969 below the three figures, warm amber, never shouting. It appears only when
1970 the live rate runs well above the user's own normal, and says nothing at
1971 all the rest of the time. */
1972.spend-governor { flex-basis: 100%; margin-top: 3px; padding: 3px 6px; border-radius: var(--radius-sm);
1973 font-size: var(--fs-3xs-2xs); text-align: center; letter-spacing: 0.01em; }
1974.spend-governor.amber { color: var(--warn); background: var(--warn-bg); }
1975.spend-governor.tripped { color: var(--danger); background: var(--warn-bg); font-weight: 600; }
1976
1977/* User identity row (foot of the rail). */
1978.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; }
1979.user-row:hover { background: var(--bg-hover); }
1980.user-avatar { color: var(--accent); font-size: var(--fs-sm); flex: none; }
1981
1982/* Message copy button (revealed on hover).
1983 The gutter is the message's own bottom padding, and it has to hold two things:
1984 the button, and the air above it. The button is 22px tall and nothing else --
1985 a 14px glyph in 3px of padding inside a 1px border -- so a 22px gutter was
1986 filled to the pixel, and the top border of a filled control landed flush on
1987 the bottom edge of a filled bubble with no line between them. The two terms
1988 are kept apart here so that resizing the glyph cannot silently close the gap
1989 again. Only the air scales with the skin: the button is the same 22px under
1990 both, but the warm skin loosens every other gap in the chat, and a gap that
1991 stayed at 6 there would be the one place left crowded. */
1992.chat-msg { position: relative; }
1993.chat-msg { --copy-gutter: calc(22px + 6px * var(--space-scale, 1)); padding-bottom: var(--copy-gutter); }
1994.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; }
1995.chat-msg-assistant .msg-copy { left: 0; }
1996.chat-msg-user .msg-copy { right: 0; }
1997.chat-msg:hover .msg-copy { opacity: 1; }
1998.msg-copy:hover { color: var(--text-primary); }
1999.msg-copy.done { color: var(--ok); border-color: var(--ok); }
2000.msg-copy .ic { width: 14px; height: 14px; }
2001
2002/* ── Turns: collapse, select, fold ──────────────────────────────────────────
2003 A question is the switch for the answers beneath it, so it looks like one:
2004 it takes a pointer, and when it is closed it says so with an ellipsis rather
2005 than silently standing next to a gap. */
2006.chat-msg-user .chat-msg-content { cursor: pointer; }
2007.chat-msg-user.collapsed .chat-msg-content::after {
2008 content: ' ⋯';
2009 color: var(--accent-text);
2010 opacity: 0.55;
2011 font-weight: 600;
2012}
2013
2014/* The tick that chooses a turn. It exists only in select mode, because outside
2015 it there is nothing it could mean.
2016 It centres on the BUBBLE, not on the box: the box carries the copy button's
2017 gutter below the bubble, and a plain 50% put the tick half a gutter low --
2018 beside the bottom edge of a one-line question rather than beside its middle. */
2019.turn-pick { position: absolute; right: 0; top: calc((100% - var(--copy-gutter, 0px)) / 2); transform: translateY(-50%); display: none; margin: 0; }
2020.turn-pick input { accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }
2021.chat-output.selecting .turn-pick { display: block; }
2022.chat-output.selecting .chat-msg-user { padding-right: 26px; }
2023.chat-output.selecting .chat-msg-user .msg-copy { right: 26px; } /* out from under the tick */
2024.chat-msg-user.picked .chat-msg-content { outline: 2px solid var(--accent); outline-offset: 1px; }
2025
2026/* Header chips: a word, not a picture. `.on` is the latched − in select mode. */
2027.chip-btn {
2028 background: var(--bg-tertiary);
2029 border: 1px solid var(--border-strong);
2030 color: var(--text-secondary);
2031 border-radius: var(--radius-sm);
2032 font-size: var(--fs-xs);
2033 padding: 3px 8px;
2034 line-height: 1.5;
2035 white-space: nowrap;
2036}
2037.chip-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
2038.chip-btn.on { background: var(--accent); border-color: var(--accent); color: var(--on-fill, #fff); }
2039.chip-btn.accent { border-color: var(--accent); color: var(--accent); }
2040.chip-btn.accent:hover { background: var(--accent); color: var(--on-fill, #fff); }
2041.chip-btn.dim { opacity: 0.45; }
2042.csel { display: flex; align-items: center; gap: 4px; }
2043
2044/* Select mode borrows the header, so the header lends it the room: the model
2045 picker and the running cost answer questions nobody is asking while choosing
2046 which turns to fold, and without this the chat's own name is crushed to "Ch…".
2047 Steps stands aside too — in select mode every turn is closed, so there are no
2048 tool steps on screen for it to show or hide. */
2049.chead.selecting .cmeter,
2050.chead.selecting #steps-toggle-btn,
2051.chead.selecting .chat-model-select { display: none; }
2052
2053.pending-centre h2 { color: var(--text-primary); }
2054
2055/* The Agents panel used to hide itself until a Diamond dispatched its first
2056 agent, and `body.agents-hidden` is what did it. The hiding was removed because
2057 a panel that comes and goes reads as a panel that was taken away -- the user
2058 asked where it had gone -- so it is now reachable at all times and says that
2059 nothing is running. Nothing sets the class any more, and these rules went with
2060 it: a selector nobody can match is a rule that will be read as live. The
2061 auto-reveal on first dispatch is unchanged and lives in daimond.js. */
2062
2063/* Identity modal (create / unlock). */
2064#identity-modal .login-logo { height: 54px; margin: 0 auto 14px; display: block; }
2065/* The wordmark ships white-lettered. The script swaps it per theme too (via
2066 #id-logo), but CSS `content` applies at first paint and outranks the src
2067 attribute, so the light and candy themes are right before any script runs. */
2068:root[data-ink="dark"] #identity-modal .login-logo {
2069 content: url("../assets/daimond_word_dark.svg");
2070}
2071#identity-modal h2 { margin-bottom: 8px; }
2072#identity-modal .cfg-lead { margin: 0 0 12px; }
2073
2074/* ── The front door ─────────────────────────────────────────────
2075 Three routes for a stranger, above the form, drawn only for a browser that
2076 has never held an identity (`syncDoors`, js/daimond.js).
2077
2078 Its budget is HEIGHT. The create card already runs past a laptop's viewport
2079 and scrolls, and the form below it is what a returning stranger comes back
2080 for -- so this is a head, one line, and one wrapped row of three, and the
2081 row wraps rather than shrinking the names, because a route whose name is
2082 cut is a route nobody takes.
2083
2084 The rule is `.id-doors`, not `#id-doors`, so a second host -- a landing
2085 panel, a phone sheet -- can wear the same strip without a new selector.
2086 `<a>` and `<button>` in the same row: two of the three leave the app and one
2087 raises a dialog, and the markup says which is which while `.id-door` makes
2088 them read as one set of choices. */
2089.id-doors {
2090 border: 1px solid var(--border-strong);
2091 border-radius: var(--radius-sm);
2092 background: var(--bg-tertiary);
2093 padding: 9px 12px 10px;
2094 margin: 0 0 12px;
2095}
2096/* The question is the first half of the lead's own sentence, so it is set on the
2097 same line rather than as a head above it. Stacked, it cost a whole line for
2098 three words and the strip stood 176px tall -- which is what pushed the create
2099 button off the bottom of the card the night the strip was added. It keeps its
2100 own key and its own weight, so it still reads as the thing being asked. */
2101.id-doors-head { font-weight: 600; color: var(--text-primary); }
2102.id-doors-lead { font-size: var(--fs-xs); color: var(--text-secondary); margin: 0 0 7px; line-height: 1.45; }
2103.id-doors-row { display: flex; flex-wrap: wrap; gap: 6px; }
2104/* One appearance for a link and a button, so the three read as three routes
2105 rather than as two links and a control. `flex: 1 1 auto` with a floor lets
2106 two sit on a line and the third drop under them on a narrow card, which is
2107 what a 366px phone does with these three names. */
2108.id-door {
2109 flex: 1 1 auto; min-width: 8.5em;
2110 display: inline-flex; align-items: center; justify-content: center;
2111 text-align: center; text-decoration: none;
2112 background: var(--bg-secondary);
2113 border: 1px solid var(--border-strong);
2114 border-radius: var(--radius-sm);
2115 color: var(--text-primary);
2116 padding: 8px 10px;
2117 font-size: var(--fs-xs);
2118 line-height: 1.2;
2119 cursor: pointer;
2120}
2121.id-door:hover { border-color: var(--accent); }
2122.id-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
2123/* "I have a passcode" is a disclosure: it opens the note below it and closes it
2124 again. Held open, it wears the accent it wears under the pointer, so the
2125 second press is offered rather than guessed at. */
2126.id-door[aria-expanded="true"] { border-color: var(--accent); background: var(--accent-soft); }
2127/* The answer given to somebody who presses "I have a passcode" before there is
2128 a key on this device to redeem it onto. `--text-secondary` rather than a
2129 dimmed muted: it is the sentence that tells them what to do next. */
2130.id-doors-note { font-size: var(--fs-xs); color: var(--text-secondary); margin: 8px 0 0; line-height: 1.45; }
2131/* The account switcher on the unlock screen: whose passphrase to enter, or add another. */
2132.id-accounts { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
2133.id-accounts-lead { font-size: var(--fs-xs); color: var(--text-muted); }
2134.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); }
2135.id-account:hover { border-color: var(--accent); }
2136.id-account.on { border-color: var(--accent); background: var(--accent-soft); }
2137.id-account-name { font-weight: 600; }
2138.id-account-fp { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); }
2139.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); }
2140.id-account-add:hover { border-color: var(--accent); color: var(--text-primary); }
2141
2142.id-fields { display: flex; flex-direction: column; gap: 10px; }
2143.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); }
2144.id-fields input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2145/* Unlocking: the name is not a field to fill but a statement of whose account is
2146 being opened. It stays a real input because that is what lets a password
2147 manager file the entry under a username, but it reads as information. */
2148.id-fields input[readonly] { color: var(--text-muted); border-style: dashed; }
2149.id-fields input[readonly]:focus { border-color: var(--border); }
2150/* The passphrase field carries a reveal toggle inside its right edge, so a long
2151 passphrase typed on a phone can be checked. The input reserves room for it. */
2152.pass-wrap { position: relative; display: flex; }
2153/* THE RESERVATION IS PART OF THE FIELD, and it has to survive a skin. Any skin
2154 that restates `padding` on inputs -- warm does, in `calc(9px * scale)
2155 calc(13px * scale)` -- wipes the right-hand reservation with the shorthand,
2156 and the typed passphrase then runs under the eye by ~17px at the default
2157 reading size. A skin cannot be expected to know which fields carry something
2158 inside them, so the field insists rather than the skin remembering. Measured
2159 with dev/probe_passeye.mjs, which reports the overlap in pixels. */
2160.pass-wrap input { flex: 1; padding-right: 40px !important; }
2161.pass-eye { position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
2162 width: 32px; height: 32px; padding: 0; display: flex; align-items: center;
2163 justify-content: center; background: none; border: none; color: var(--text-muted);
2164 cursor: pointer; }
2165.pass-eye:hover { color: var(--text-primary); }
2166.pass-eye .ic { width: 20px; height: 20px; }
2167/* ── The foot that holds the one next step ──────────────────────
2168 `.modal-card` scrolls (`overflow-y: auto`), and the create screen is the
2169 tallest thing that has ever been put in one: a logo, the stranger strip, a
2170 title, a lead, a name, a generated passphrase with its note and its
2171 acknowledgement, a passphrase field, a confirmation, and only then the button
2172 that does the thing. On 2026-08-14, at 1500x950, it ran 1086px inside an 872px
2173 box and "Create account" sat at 914-959 with the card's own edge at 912 --
2174 `elementFromPoint` over the middle of the button answered `#identity-modal`. A
2175 first-time visitor on a laptop could not see the button they had been sent
2176 here to press. Nor could one on a phone, and that half is older than the front
2177 door: at 375x812 the card was over its height by ~140px with the strip hidden
2178 altogether. `dev/l11_fold.mjs` measures both.
2179
2180 So the foot STICKS to the bottom of the scroll port: whatever else is under
2181 the fold, the next step is on screen. Sticky and not a flex footer because the
2182 card is shared furniture -- a real footer means splitting every modal into a
2183 scrolling body and a fixed foot, and this is one screen's problem.
2184
2185 It costs nothing when there is room. Sticky only lifts an element that would
2186 otherwise sit below the port, so on the UNLOCK screen -- half this height, no
2187 scrollbar -- the button stays exactly where it was. Its containing block is
2188 the form, so it can rise no further than the form's own top.
2189
2190 The padding is the gap the button used to carry as `margin-top`. A margin
2191 would be transparent, and the form would scroll through the 12px band above a
2192 stuck button; padding on the foot paints the card's own ground there instead.
2193 `--bg-secondary` is that ground: `.modal-card` sets it and no skin restates
2194 it, so this follows the card through both skins and every palette.
2195
2196 AND THE CARD'S BOTTOM PADDING BECOMES A SPACER, three rules below. A stuck
2197 element stops at the scroll port's CONTENT edge, while a scroll container's
2198 own end padding is scrollable area -- so the card's 24px (34px warm, 16px on
2199 a phone) was a band under the button that the acknowledgement row slid
2200 through, photographed on a 375px phone in the warm skin. Covering it from the
2201 foot cannot work: anything that reaches past the foot's box paints over
2202 "Choose my own passphrase instead" once the card is scrolled to the end and
2203 the foot is no longer stuck. Moving the same space to a `::after` puts the
2204 card's inner bottom edge and its content edge in the same place, so there is
2205 no band to cover, and the tail keeps the air it had. */
2206.id-foot {
2207 position: sticky; bottom: 0;
2208 background: var(--bg-secondary);
2209 padding: 12px 0 6px;
2210}
2211#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); }
2212#id-primary:hover { background: var(--accent-hover); }
2213#id-primary:disabled { opacity: .5; cursor: not-allowed; }
2214#id-primary:disabled:hover { background: var(--accent); }
2215
2216/* The generated passphrase. It is READ here, not typed, so it is set in the
2217 mono face at a size that survives being copied onto paper, and given room to
2218 wrap across three lines rather than scrolling sideways in a one-line box. */
2219/* The create screen carries more than any other modal — a name, the generated
2220 passphrase with its note and acknowledgement, the field, and two ways out — so
2221 it is given more of the window than the shared 80vh before it starts scrolling.
2222 A short window still scrolls, which is correct; this only stops a full-height
2223 one from hiding the button and the escape hatch under the fold. */
2224#identity-modal .modal-card { max-height: 92vh; padding-bottom: 0; }
2225/* The bottom padding, moved off the card and into its last line, so a stuck
2226 `.id-foot` has nothing scrolling underneath it (see the foot above). The
2227 number is the card's own: 24px, times `--space-scale` where a skin sets one --
2228 which is how skin-warm.css writes the very padding this replaces. An id beats
2229 both the skin's selector and the phone's, so this holds everywhere; a phone
2230 therefore ends 8px deeper than mobile.css's 16px, which is the one place this
2231 is not a straight swap and is below noticing at the end of a scroll. */
2232#identity-modal .modal-card::after {
2233 content: "";
2234 display: block;
2235 height: calc(24px * var(--space-scale, 1));
2236}
2237.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; }
2238.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; }
2239.pass-gen-acts { display: flex; gap: 8px; }
2240.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; }
2241.pass-gen-btn:hover { border-color: var(--accent); color: var(--text-primary); }
2242.pass-gen-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }
2243.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; }
2244.pass-gen-ack input { flex: none; margin-top: 1px; accent-color: var(--accent); }
2245
2246/* The passkey button and its note are styled in passkey.css, which owns them. */
2247
2248/* "Choose my own passphrase instead" — a way out of the generated phrase that
2249 is deliberately quieter than the button that accepts it. */
2250.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; }
2251.id-choose:hover { color: var(--text-primary); }
2252
2253/* Chat input: grow to ~12 lines, scrollbar only on hover/focus. */
2254/* The placeholder and a single line of text sit CENTRED in the box. The old rule
2255 zeroed the bottom padding, which left all 9px of slack at the top and dropped the
2256 one line to the floor of the box — the "too low" placeholder. Splitting it 5/4
2257 keeps the same 32px resting height (21 line + 9 padding + 2 border) with the line
2258 in the middle, and the textarea still grows from there. */
2259#chat-input { max-height: 263px; padding-top: 5px; padding-bottom: 4px; overflow-y: hidden; }
2260#chat-input:hover, #chat-input:focus { overflow-y: auto; }
2261#chat-input::-webkit-scrollbar { width: 8px; }
2262#chat-input::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
2263#chat-input::-webkit-scrollbar-track { background: transparent; }
2264
2265
2266/* ── In-app dialogs ──────────────────────────────────────────────
2267 The replacements for window.prompt / confirm / alert. They sit on the
2268 same .modal / .modal-card furniture as Settings, so a destructive
2269 question looks like part of Daimond rather than an OS box quoting the
2270 origin over a dark UI. */
2271.dlg-card { width: 420px; }
2272/* `pre-wrap` and `anywhere` are both about the consent dialogs, which QUOTE the
2273 thing being authorised inside the sentence asking about it. The quote is no
2274 longer shortened, so two things have to hold however long it is: the blank
2275 lines the string was written with must survive, or the quote runs into the
2276 sentence and its edges cannot be seen; and an unbroken run -- a token, a card
2277 number, a base64 blob -- must wrap rather than overflow a 420px card and hide
2278 exactly the part worth reading. */
2279.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; }
2280.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); }
2281/* `.dlg-input` is worn by the dialog's text fields AND by its pulldown
2282 (`.dlg-input.dlg-select`), so it takes `:focus-visible` for the pulldown's
2283 sake -- see the note at `.settings-select`. On the text fields it resolves to
2284 exactly what `:focus` did. */
2285.dlg-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2286/* A models pulldown in a dialog: the same box as the text fields beside it, so the
2287 form reads as one form. A long model id is allowed to be long -- truncating it
2288 would hide the very part that tells two of them apart. */
2289.dlg-select { font-family: var(--font-mono); font-size: var(--fs-sm); }
2290/* The UI face, not --font-sans: that token does not exist, so the declaration
2291 was invalid and the group heading inherited the mono face of the options it
2292 was meant to stand apart from. */
2293.dlg-select optgroup { font-family: var(--font); font-style: normal; font-weight: 600; color: var(--text-secondary); }
2294.dlg-select option { font-family: var(--font-mono); color: var(--text-primary); }
2295.dlg-select option:disabled { color: var(--text-muted); }
2296/* A dialog's own tick: one thing the user turns ON, above the buttons. It is a
2297 `label` wrapping the box, so the words are part of the target -- a consent
2298 control whose only hit area is a 13px square is one people miss and then say
2299 they never chose. Unticked at every open, because the dialog is built fresh
2300 each time and a remembered tick would be the standing answer arriving by the
2301 back door again. */
2302.dlg-tick { display: flex; align-items: flex-start; gap: 9px; margin: 2px 0 14px; cursor: pointer;
2303 font-size: var(--fs-sm); color: var(--text-secondary); line-height: 1.4; }
2304.dlg-tick-box { margin: 1px 0 0; flex: none; accent-color: var(--accent); }
2305.dlg-tick:hover { color: var(--text-primary); }
2306.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); }
2307.dlg-ok:hover { filter: brightness(1.08); }
2308.dlg-err { min-height: 16px; font-size: var(--fs-xs); color: var(--danger); margin-bottom: 8px; }
2309/* The pair wraps rather than bursting. These two are `flex: 1` off a zero basis,
2310 but a one-word label cannot shrink below itself, so the row's real minimum is
2311 both labels plus four lots of side padding -- and the drawer that hosts the
2312 same form in the rail is about 190px wide, against a card's 420. "Add and
2313 sync" beside "Cancel" does not fit that, and the drawer clips what it cannot
2314 hold (`.admin-scroll` is overflow-x: hidden), so the primary button lost its
2315 right-hand end. Wrapping costs nothing where the row already fits: the buttons
2316 only take a line each when the alternative was losing one of them. */
2317.dlg-actions { display: flex; flex-wrap: wrap; gap: 8px; }
2318.dlg-actions .dlg-cancel { margin-top: 0; flex: 1; }
2319.dlg-actions .dlg-ok { margin-top: 0; flex: 1; width: auto; }
2320.dlg-actions .dlg-ok.danger { background: var(--danger); border-color: var(--danger); color: var(--on-fill, #fff); }
2321.dlg-actions .dlg-ok.danger:hover { filter: brightness(1.1); }
2322.tile-fold.folded { color: var(--text-secondary); border-style: dashed; }
2323
2324/* ── About ───────────────────────────────────────────────────────
2325 Inkscape's shape: the splash artwork owns the top of the card and reaches its
2326 edges, the identity sits under it, and the small print is a quiet row along
2327 the foot. It is the `.modal-card` / `.dlg-card` furniture every other dialog
2328 wears, with a class of its own for the parts that are only true here. */
2329/* `--about-pad` mirrors the card's own padding, and exists because the phone
2330 changes it (mobile.css drops `.modal-card` to 16px). Two bleeding children
2331 below cancel that padding out, and a bleed measured against the wrong number
2332 is a picture hanging over the edge of its card. */
2333.about-card { width: 480px; --about-pad: 24px; position: relative; }
2334/* The closer, over the splash rather than under it. Every other card in the app
2335 puts its cross in a title row above the content; this card's content STARTS
2336 with a full-bleed picture, so a row above it would push the artwork off the
2337 top edge and a row below it would put the way out halfway down the card.
2338 Absolute here and nowhere else: it is positioned against the card, and it
2339 scrolls with the picture it sits on, which is the behaviour that made
2340 absolute wrong everywhere the content is text.
2341
2342 The heading in this row is `.vh` -- About is titled by its artwork -- so the
2343 row collapses to the cross and needs no width of its own. */
2344.about-card > .ui-head {
2345 position: absolute; top: 8px; right: 8px; z-index: 1;
2346 margin: 0; gap: 0;
2347}
2348/* The splash brings its own near-black ground, so the muted grey every other
2349 closer wears would sink into it. A dark disc under white ink reads on the
2350 artwork whatever the palette, because the artwork is fixed-colour too. */
2351.about-card > .ui-head .ui-close {
2352 background: rgba(0, 0, 0, 0.45);
2353 color: #fff;
2354 border-radius: 999px;
2355}
2356.about-card > .ui-head .ui-close:hover { background: rgba(0, 0, 0, 0.7); color: #fff; }
2357/* Full bleed. The card pads its contents and the splash is a picture, not a
2358 paragraph -- a margin round it reads as an accident.
2359
2360 The width is the content box PLUS both paddings, which is exactly the padding
2361 box, so nothing overflows. That matters more than it looks: `.modal-card` is
2362 `overflow-y: auto`, and CSS computes `overflow-x: visible` to `auto` when the
2363 other axis is not visible -- so a splash one pixel wider than its padding box
2364 would hang a horizontal scrollbar under the picture on every open. */
2365.about-splash {
2366 display: block;
2367 width: calc(100% + var(--about-pad) * 2);
2368 /* Stated, not inherited: a splash squashed to a fixed height is the classic
2369 way this goes wrong, and the artwork's own 863x448 is what decides it. */
2370 height: auto;
2371 margin: calc(var(--about-pad) * -1) calc(var(--about-pad) * -1) 0;
2372 border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
2373}
2374.about-body { display: flex; flex-direction: column; align-items: center; gap: 12px; padding-top: 18px; }
2375/* The wordmark, at about twice the height the top bar draws it. `.brand-wordmark`
2376 is carried alongside for the light/dark swap, which is written against that
2377 class; the height here wins because it is the later rule. */
2378.about-word { height: 46px; width: auto; }
2379.about-said {
2380 font-size: var(--fs-base); color: var(--text-secondary);
2381 line-height: 1.55; text-align: center; margin: 0; max-width: 34em;
2382}
2383/* The version, said the way the rail's status panel says it: the same label
2384 word, the id in the mono face, and the app's one copy button beside it. */
2385.about-ver { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: var(--fs-xs); }
2386.about-ver-label { color: var(--text-muted); }
2387.about-build { font-family: var(--font-mono); color: var(--text-secondary); overflow-wrap: anywhere; }
2388/* The foot. A rule across the card, then the signature -- which is what the
2389 `100% + 48px` is doing again: the line spans the card, the badge does not. */
2390.about-maker {
2391 display: flex; align-items: center; justify-content: center;
2392 width: calc(100% + var(--about-pad) * 2);
2393 margin: 6px calc(var(--about-pad) * -1) 0;
2394 padding: 14px var(--about-pad) 2px;
2395 border-top: 1px solid var(--border);
2396}
2397/* ONE rule across the foot, not two. `.tile-dlg-foot` draws its own, which is
2398 right where the foot is the first thing under the body -- here the signature
2399 is already below a rule, and a second line 60px under the first read as a
2400 mistake in every palette. */
2401.about-card .tile-dlg-foot { border-top: none; margin-top: 10px; padding-top: 0; }
2402/* The maker's badge. One lozenge with its own white plate and its own border,
2403 so it takes no palette treatment and needs no dark/light twin -- it looks the
2404 same on every theme, which is the point of a mark. */
2405.made-by { position: relative; display: flex; align-items: center; flex: none; }
2406/* The two halves of the badge, laid over the artwork. Each is a real element
2407 with its own name, so a screen reader meets two claims rather than one
2408 picture, and each is a link: the flame to Oxedyne, the chip to the
2409 AI-disclosure mark it is claiming. `verify_badge.mjs` reads the 50% out of
2410 this rule and measures the artwork against it, so the two cannot drift. */
2411.made-by .mb-hit { position: absolute; top: 0; bottom: 0; width: 50%; display: block; }
2412.made-by .mb-oxedyne { left: 0; }
2413.made-by .mb-ai { right: 0; }
2414/* The whole plate dims under either half, because the artwork is one file and
2415 there is no half of it to dim on its own. Written on the parent rather than as
2416 `.mb-oxedyne:hover ~ img`, which was what stood here and matched nothing: the
2417 image is the FIRST child, so no sibling combinator can reach back to it. */
2418.made-by:has(.mb-hit:hover) img { opacity: 0.85; }
2419.made-by .mb-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
2420.made-by img { height: 40px; width: auto; display: block; transition: opacity 0.15s ease; }
2421
2422/* ── A tile's own dialog (the cog) ───────────────────────────────
2423 Same `.modal` / `.modal-card` furniture as every other dialog, so Escape,
2424 the backdrop and the focus trap behave the way they do everywhere. What is
2425 different is the foot: it holds Delete, and only Delete. The way out of the
2426 dialog is the cross in the title row, which is where a person looks for it;
2427 a full-width "Done" at the bottom of a scrolling card is a button you have to
2428 travel to in order to do nothing. */
2429.tile-dlg-head {
2430 font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em;
2431 color: var(--text-muted); margin: 14px 0 6px;
2432}
2433.tile-dlg-card h2 { margin-bottom: 4px; overflow-wrap: anywhere; }
2434/* The title row -- the Diamond's name, and the way out -- is `.ui-head` now;
2435 the element carries both classes so the selectors written against this one
2436 keep working. */
2437/* The closer. Its shape, its hover ground, its focus ring and its touch floor
2438 are all `.ui-close` now — this dialog was where the app's cross was worked
2439 out, and the rest of the app has since caught up with it. The class survives
2440 as the hook `shot_tiledlg` reaches for and as the place to say anything true
2441 of THIS cross alone. */
2442.tile-dlg-pause { display: flex; align-items: center; gap: 8px; }
2443.tile-dlg-pause-words { font-size: var(--fs-sm); color: var(--text-secondary); }
2444.tile-dlg-seg { display: flex; gap: 6px; }
2445/* The colour reset closes the section above it, so it needs the air a section
2446 break has. Without this it sat directly under the Text swatch and read as a
2447 third row of the same control. */
2448.tile-dlg-seg > .tile-dlg-clear { margin-top: 10px; }
2449/* One labelled row in the tile dialog -- a model, a colour, anything with a
2450 name on the left and a control on the right. `.tile-dlg-model` is the model
2451 rows' own name and is listed beside it rather than replaced: the two classes
2452 are emitted together, and either one alone still gets the row's shape. */
2453.tile-dlg-field,
2454.tile-dlg-model { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
2455/* The label column. A FIXED width, not a minimum: "Workers, images" is longer
2456 than "Daimon" and a min-width let it push its pulldown right, so three rows
2457 that are one setting read as three unrelated ones. The label ellipsises
2458 rather than moving the column it exists to line up.
2459 It is set in the UI font at the dialog's own reading size, in sentence case,
2460 and NOT in the small uppercase `.trig-label` wears. `.tile-dlg-head` -- the
2461 section heading directly above these rows -- is already 11px uppercase muted
2462 with the same letter-spacing, so a label drawn that way would be
2463 indistinguishable from the heading over it and "MODELS / DAIMON / WORKERS"
2464 would read as one flat list of small caps rather than a heading and the
2465 fields under it. Two levels want two treatments.
2466 `.tile-model-chip` is the selector the model rows used before they had a real
2467 <label>, and is kept here so the column survives either markup. That class is
2468 the badge a TILE wears to say which model is running -- monospace, a pill, a
2469 ground and a border -- because there it is a piece of data; in a form's label
2470 column it is a badge with nothing in it. The pill comes off either way. */
2471.tile-dlg-label,
2472.tile-dlg-model .tile-model-chip {
2473 flex: 0 0 116px; width: 116px;
2474 font-family: var(--font); font-size: var(--fs-sm);
2475 color: var(--text-secondary);
2476 background: none; border: 0; border-radius: 0;
2477 padding: 0; text-align: left;
2478 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2479}
2480/* Only the real <label> claims to be pressable. The legacy span is not, and a
2481 span drawn with a pointer that no key can reach is what verify_a11y_keyboard
2482 calls a ghost. */
2483.tile-dlg-label { cursor: pointer; }
2484/* The pulldown is `.tile-model`, which is sized for a tile -- 12px type in a
2485 5px box, which is right in a 260px rail and undersized in a dialog whose
2486 other controls are 13px in a 8px box. It takes the dialog's measure here. */
2487.tile-dlg-field .tile-model,
2488.tile-dlg-model .tile-model {
2489 flex: 1 1 auto; min-width: 0;
2490 font-size: var(--fs-sm);
2491 /* Room on the right for the browser's own arrow. A native select draws it
2492 inside the padding box, so with a symmetric 8px it sits all but touching
2493 the border and reads as a rendering fault rather than a control. The left
2494 stays at 8px: only the arrow side needs the air. */
2495 padding: 7px 22px 7px 8px;
2496 border-color: var(--border-strong);
2497}
2498/* A tile's colour. The browser draws the well itself, so the job here is to
2499 stop it drawing the CHROME around it: left alone, `input[type=color]` comes
2500 with a platform border and a grey plate that belong to no theme in this app.
2501 The element keeps the app's own border and radius and 2px of padding, which
2502 is what turns the native swatch into an inset chip rather than a flat block
2503 with a line round it. Both engines' inner pseudo-elements are named because
2504 Firefox and Chromium spell them differently and each ignores the other's. */
2505.tile-dlg-swatch {
2506 flex: none;
2507 width: 52px; height: 28px; padding: 2px;
2508 background: var(--bg-tertiary);
2509 border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
2510 cursor: pointer;
2511}
2512.tile-dlg-swatch::-webkit-color-swatch-wrapper { padding: 0; }
2513/* The FILL has to follow the frame's corner, less the 2px inset that separates
2514 them. Hard-coded at 2px it sat as a near-square inside a 6px radius -- and
2515 inside the warm skin's 10px, which is where it read as a mistake rather than
2516 as a detail. `calc()` keeps the two in step if the token ever moves. */
2517.tile-dlg-swatch::-webkit-color-swatch { border: 0; border-radius: calc(var(--radius-sm) - 2px); }
2518.tile-dlg-swatch::-moz-color-swatch { border: 0; border-radius: calc(var(--radius-sm) - 2px); }
2519.tile-dlg-swatch:hover { border-color: var(--accent); }
2520.tile-dlg-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
2521.tile-dlg-level {
2522 flex: 1;
2523 background: var(--bg-tertiary); border: 1px solid var(--border-strong);
2524 border-radius: var(--radius-sm); color: var(--text-secondary);
2525 padding: 8px 10px; font-size: var(--fs-sm);
2526}
2527.tile-dlg-level:hover { border-color: var(--accent); color: var(--text-primary); }
2528.tile-dlg-level[aria-pressed="true"] {
2529 background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text);
2530}
2531.tile-dlg-level:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
2532/* "Use the theme's colours": the way back from a colour that was chosen. It
2533 wears the same box as `.tile-dlg-level` and states it rather than inheriting
2534 it, so it does not depend on that class outliving the control it was written
2535 for. `flex: 0 0 auto` and not the segment's `flex: 1`: a segment divides its
2536 width between choices that are alternatives to each other, and a lone reset
2537 is not one of a set -- stretched across the row it would read as the row's
2538 main action, which is the opposite of what it is.
2539 It must stay BELOW `.tile-dlg-level`: an element carrying both classes takes
2540 whichever comes last, and `flex: 1` winning here is the whole defect. */
2541.tile-dlg-clear {
2542 flex: 0 0 auto;
2543 background: var(--bg-tertiary); border: 1px solid var(--border-strong);
2544 border-radius: var(--radius-sm); color: var(--text-secondary);
2545 padding: 8px 10px; font-size: var(--fs-sm);
2546}
2547.tile-dlg-clear:hover { border-color: var(--accent); color: var(--text-primary); }
2548.tile-dlg-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
2549/* The daimon's "Change". Filled, because it IS the affirmative -- pressing it
2550 switches the model -- and it shows only while the pick differs, so it never
2551 sits idle beside the pulldown inviting a press that does nothing. Beside it the
2552 bordered `.tile-dlg-clear` reads as the quieter act it is. */
2553.tile-dlg-apply {
2554 flex: 0 0 auto;
2555 background: var(--accent); color: var(--on-fill, #fff); border: none;
2556 border-radius: var(--radius-sm); padding: 8px 12px; font-size: var(--fs-sm);
2557}
2558.tile-dlg-apply:hover { background: var(--accent-hover); }
2559.tile-dlg-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
2560.tile-dlg-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; margin-top: 8px; }
2561/* The same note in the palette's warning colour, for the one that says a chosen
2562 pair of colours cannot be read. Colour ALONE, with no frame and no icon: it is
2563 a remark, not a refusal, and the sentence carries the whole meaning for a
2564 reader who sees no colour at all. The muted grey it replaces is deliberately
2565 the only thing that changes -- a bordered callout here would read as an error,
2566 and the choice is not an error. */
2567.tile-dlg-note.warn { color: var(--warn); }
2568/* Saving a template. The checkbox row reads left to right -- box then words --
2569 because it is a choice about what goes in, not a labelled field; the button
2570 below it takes its own width, since a control stretched across a dialog is
2571 the one a person presses without reading it. */
2572.tile-dlg-tmpl { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
2573.tile-dlg-check { accent-color: var(--accent); width: 15px; height: 15px; flex: 0 0 auto; }
2574/* THE WORDS, NOT A LABEL COLUMN. `.tile-dlg-label` is 116px wide with an
2575 ellipsis, which is right beside a field it names and wrong for a checkbox
2576 whose whole meaning is in its own sentence: it drew "Include what it ha…".
2577 Here the words take the rest of the row and wrap. */
2578.tile-dlg-tmpl .tile-dlg-label {
2579 flex: 1 1 auto; width: auto;
2580 white-space: normal; overflow: visible; text-overflow: clip;
2581 cursor: pointer;
2582}
2583/* Saving is the SECTION's main action, not the dialog's, and directly below it
2584 sits Delete — filled, in the danger colour, and the one thing in here that
2585 cannot be undone. Two filled buttons a thumb apart is how the wrong one gets
2586 pressed, so this one wears a box rather than a fill. Its own rule and not
2587 `.tile-dlg-clear`'s, for the reason that class states about itself: a control
2588 borrowing another's styling depends on it outliving the control it was
2589 written for. */
2590.tile-dlg-tmpl-save {
2591 flex: 0 0 auto; align-self: flex-start; margin-top: 12px;
2592 background: var(--bg-tertiary); border: 1px solid var(--border-strong);
2593 border-radius: var(--radius-sm); color: var(--text-secondary);
2594 padding: 8px 14px; font-size: var(--fs-sm);
2595}
2596.tile-dlg-tmpl-save:hover:not(:disabled) { border-color: var(--accent); color: var(--text-primary); }
2597.tile-dlg-tmpl-save:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
2598.tile-dlg-tmpl-save:disabled { opacity: 0.55; cursor: default; }
2599.tile-dlg-say[hidden] { display: none; }
2600
2601/* The foot. Delete takes what it needs and no more -- it is the one thing in
2602 here that cannot be undone, and a button stretched across the width of a
2603 dialog is the button a person presses without reading it. Where a dialog
2604 still ends in Done (`dialog()` builds one for the chat tile and others), Done
2605 takes the rest of the row and Delete keeps its own width beside it. */
2606.tile-dlg-foot { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 14px; }
2607.tile-dlg-foot .tile-dlg-delete { flex: 0 0 auto; }
2608.tile-dlg-foot .tile-dlg-done { flex: 1; }
2609
2610
2611/* Locked: lockApp() empties the panels of the user's content, so the shell
2612 is left standing but bare. Nothing of theirs is on screen behind the card. */
2613body.locked .tile-fold, body.locked .spend-row { display: none; }
2614
2615/* ── The account's own controls ──────────────────────────────────
2616 These were a floating menu anchored to the user row. They live in the Admin
2617 panel's home view now: the device fingerprint is worth seeing, not worth a
2618 popup. */
2619/* The fingerprint, with the button that copies it. A flex row rather than a
2620 block, so the button keeps its place while the value takes whatever width is
2621 left: as a block, the value's own ellipsis ate the button on a narrow rail. */
2622.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; }
2623.account-fp-val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2624
2625/* The devices that sync this account. A row is a line of text with two quiet
2626 controls on it: name it, or take it off the list.
2627 Removing is not revoking, and the styling should not imply that it is.
2628 Pairing hands the second device the SAME keypair, so nothing here can lock a
2629 device out; the cross tidies a register of what has been seen. It is drawn as
2630 the milder of the two acts on the row -- a danger colour on hover, and no
2631 ground of its own -- rather than as a destructive button. */
2632.device-row { display: flex; align-items: baseline; gap: 6px; padding: 5px 8px; font-size: var(--fs-sm); color: var(--text-primary); }
2633/* The name shrinks first and the id takes the slack, so the id sits with the
2634 name it belongs to and the last-seen column stays flush right. */
2635.device-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2636.device-id { flex: 1 1 auto; font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-muted); }
2637.device-when { flex: none; font-size: var(--fs-2xs); color: var(--text-muted); white-space: nowrap; text-align: right; }
2638/* Naming a device, and removing one. Muted until the row is under the pointer
2639 or the button has the keyboard, so a column of pencils does not compete with
2640 the names. The tap target stays 24px square whatever the reading size.
2641 Off the row's baseline, and only this one: three sizes of text SHOULD share a
2642 baseline, which is what the row asks for, but a 24px box has no business on
2643 one. Baseline-aligned it hung by the pencil's own baseline -- so the box
2644 overshot the line top and bottom, taking the row's height with it, and where
2645 the glyph sat depended on which font the platform found '✎' in. Centred, the
2646 square is the row's and the glyph can come from anywhere.
2647 The type size is LARGER than the row it sits on, which for a word would be
2648 wrong and for this glyph is what makes it legible: '✎' is a dingbat and is
2649 drawn well inside its em box, so at the row's own 13px the pencil came out
2650 around 8px of actual ink and read as a smudge. 18px of box gives it roughly
2651 the ink a 13px letter has. The 24px target is untouched. The remove cross
2652 takes the identical box: two controls of different sizes side by side on
2653 every row would be a worse fault than either of them being wrong. */
2654.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; }
2655.device-row:hover .device-rename, .device-rename:hover, .device-rename:focus-visible,
2656.device-row:hover .device-remove, .device-remove:hover, .device-remove:focus-visible { opacity: 1; color: var(--text-primary); background: var(--bg-hover); }
2657/* Removing a device is not renaming it, and the colour is the only thing on the
2658 row that says so. The row-hover selectors are repeated with the button's own
2659 state on the end: `.device-row:hover .device-remove` is more specific than a
2660 bare `.device-remove:hover`, so without them the neutral colour above would
2661 win at exactly the moment the pointer is on the cross. */
2662.device-remove:hover, .device-remove:focus-visible,
2663.device-row:hover .device-remove:hover,
2664.device-row:hover .device-remove:focus-visible { color: var(--danger); }
2665/* The always-on-runner star. It takes the same icon-button box as the pencil and
2666 cross, and is muted-until-hover the same way -- EXCEPT the current nominee, whose
2667 filled star stays lit so the one runner is visible without hovering every row. */
2668.device-nominate { 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; }
2669.device-row:hover .device-nominate, .device-nominate:hover, .device-nominate:focus-visible { opacity: 1; color: var(--text-primary); background: var(--bg-hover); }
2670.device-nominate.is-nominee { opacity: 1; color: var(--accent); }
2671/* A small badge on the nominee's line, so the runner reads at a glance beside the
2672 last-seen column rather than only through the star. */
2673.device-nominee { flex: none; font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: .04em; color: var(--accent); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px; white-space: nowrap; }
2674
2675#id-name-row { margin-bottom: 8px; }
2676#id-name { width: 100%; }
2677
2678
2679/* ── Agent tiles ─────────────────────────────────────────────────
2680 A dispatched worker: what it was told to do, what it is doing, what it
2681 cost, and a control to stop it or fold its summary into the crystal. */
2682.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; }
2683.acard.queued { opacity: 0.7; }
2684.acard.stopped { opacity: 0.75; }
2685.acard.paused { opacity: 0.85; border-color: var(--border-2); border-left: 2px solid var(--accent); }
2686.pill.queued { background: var(--bg-tertiary); color: var(--text-secondary); }
2687.pill.stopped { background: var(--bg-tertiary); color: var(--text-secondary); }
2688.pill.paused { background: var(--accent-soft); color: var(--accent-text); }
2689.aacts { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
2690.afolded { flex: 1; font-size: var(--fs-2xs); color: var(--text-muted); font-family: var(--font-mono); }
2691.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); }
2692.abtn:hover { background: var(--bg-hover); }
2693.abtn.stop, .abtn.a-cross { color: var(--danger); }
2694.abtn.stop:hover, .abtn.a-cross:hover { background: var(--danger); color: var(--on-fill, #fff); border-color: var(--danger); }
2695.abtn.a-play { color: var(--accent-text); }
2696.abtn.a-play:hover { background: var(--accent); color: var(--on-fill, #fff); border-color: var(--accent); }
2697
2698/* Agents panel header: pause / resume / stop, acting on every agent. The one
2699 brake on a runaway fan-out, so the group is prominent but quiet until there
2700 are agents to act on. */
2701.agents-ctl { display: inline-flex; gap: 3px; align-items: center; }
2702.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); }
2703.agc:hover:not(:disabled) { color: var(--text-primary); }
2704.agc:disabled { opacity: 0.3; cursor: default; }
2705#agents-stop:hover:not(:disabled) { color: var(--danger); border-color: var(--danger); }
2706#agents-play:not(:disabled) { color: var(--accent-text); border-color: var(--accent); }
2707/* When the pool is held, the resume control pulses so the paused state is not missed. */
2708.agents-ctl.holding #agents-play { background: var(--accent-soft); }
2709.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; }
2710
2711.diff-accept:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
2712.diff-accept:disabled:hover { filter: none; }
2713
2714
2715/* ── Crystal: edit and history ─────────────────────────────────────
2716 Every version was already snapshotted to disk; nothing could reach one.
2717 An accepted fold overwrites the crystal, so without this there is no undo. */
2718.crystal-bar { display: flex; gap: 6px; margin-bottom: 10px; }
2719.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); }
2720.crystal-act:hover { background: var(--bg-hover); color: var(--text-primary); }
2721.crystal-act.primary { background: var(--accent); color: var(--on-fill, #fff); border-color: var(--accent); }
2722.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; }
2723.crystal-edit:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2724
2725/* ── The crystal as a page: the chrome around the frame ────────────
2726 The frame, the built-in view that replaces a page which fails, and the note
2727 that says it failed are all drawn by css/crystal.css. What is here is what
2728 the APP puts around them -- the ask row, the ✎ form -- and not one class the
2729 frame lane also names. crystal.css is linked after this file anyway, so it
2730 would win on anything the two ever repeated. */
2731
2732/* App chrome, BELOW the frame, never inside it. A page cannot ask the user
2733 anything -- a parent cannot verify user activation across the boundary, so a
2734 timer there is indistinguishable from a click -- so the box that talks to the
2735 daimon about the page lives out here, where a click is provably a person's. */
2736
2737/* The memory as it sits on disk, shown when crystal.js did not load at all. A
2738 broken build must not look like an empty Diamond. */
2739.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; }
2740
2741/* The ✎ form. A textarea written straight through was harmless for markdown,
2742 which has no parse failure, and a data-loss path for JSON. */
2743.crystal-form { display: flex; flex-direction: column; gap: 4px; }
2744.crystal-form-row { display: flex; flex-direction: column; }
2745.crystal-form-list { display: flex; flex-direction: column; gap: 6px; }
2746.crystal-form-list > .crystal-act { align-self: flex-start; }
2747.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); }
2748.crystal-form-line { display: flex; gap: 6px; align-items: center; }
2749.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); }
2750.crystal-form-item .crystal-form-input { background: var(--bg-primary); }
2751.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; }
2752.crystal-form-item .crystal-form-area { background: var(--bg-primary); }
2753.crystal-form-input:focus, .crystal-form-area:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2754.crystal-form-input::placeholder, .crystal-form-area::placeholder { color: var(--text-muted); }
2755.crystal-form-x { flex: none; padding: 2px 8px; line-height: 1.4; }
2756
2757/* Read-only, and written back exactly as it came in. A form that drops what it
2758 cannot render is the defect this design is shaped around. */
2759.crystal-form-extra { margin-top: 12px; padding: 10px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-sm); }
2760.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); }
2761
2762/* The raw JSON, behind a second click, and it refuses to save unparseable text. */
2763.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; }
2764.crystal-json:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2765
2766/* The memory, click-to-expand and editable. The page renders the memory; this
2767 shows the memory itself. Collapsed by default, so it costs one row until asked. */
2768.crystal-memory { margin: 0 0 10px; }
2769.crystal-memory-sum { cursor: pointer; font-size: var(--fs-xs); color: var(--text-secondary); padding: 2px 0; user-select: none; }
2770.crystal-memory-sum:hover { color: var(--text-primary); }
2771.crystal-memory-ta { width: 100%; min-height: 220px; margin-top: 8px; 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; }
2772.crystal-memory-ta:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2773.crystal-memory-bar { display: flex; gap: 6px; margin-top: 8px; }
2774.hist-list { display: flex; flex-direction: column; gap: 6px; }
2775/* Wraps, so a note can take a line of its own beneath the version and its
2776 buttons rather than squeezing between them. A row with no note is unchanged:
2777 two children, one line. */
2778.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); }
2779.hist-note { flex: 1 0 100%; font-size: var(--fs-2xs); color: var(--text-muted); line-height: 1.5; overflow-wrap: anywhere; }
2780.hist-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
2781.hist-ver { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-primary); }
2782.hist-kind { font-size: var(--fs-xs); color: var(--text-secondary); }
2783.hist-when { font-size: var(--fs-2xs); color: var(--text-muted); }
2784.hist-acts { display: flex; gap: 6px; flex: none; }
2785
2786/* ── Tags ────────────────────────────────────────────────────────
2787 The user's filing system for Diamonds, and nothing more: a tag never reaches a
2788 model, a crystal or a prompt. A chip's hue is hashed from the tag name in JS
2789 and arrives as --tag-h, so one tag is one colour everywhere; saturation and
2790 lightness come from the theme, so the dark theme gets a deep chip with light
2791 lettering and the light themes get the reverse, and neither has to know
2792 which tags exist. */
2793.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;
2794 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%); }
2795button.tag-chip:hover { background: hsl(var(--tag-h, 0) 34% 27%); border-color: hsl(var(--tag-h, 0) 40% 48%); }
2796: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%); }
2797: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%); }
2798
2799/* On a Diamond box, beside v{n} and the time. The +N stands for the tags that
2800 did not fit and names them on hover.
2801
2802 Shaped like the chips it stands for, in a neutral. It was bare monospace grey
2803 beside three coloured pills, and when the meta row wrapped it landed alone on
2804 the second line under `v0` and read as debris rather than as "and two more
2805 tags". Same pill, same size, no hue — it is not a tag, it is a count of
2806 them. */
2807.tag-more {
2808 display: inline-flex; align-items: center;
2809 font-family: var(--font); font-size: var(--fs-3xs); line-height: 1.6;
2810 padding: 0 6px; border-radius: 20px; white-space: nowrap;
2811 background: var(--bg-tertiary); color: var(--text-muted);
2812 border: 1px solid var(--border); cursor: default;
2813}
2814.tag-active { padding-right: 3px; }
2815/* The closer owns the pill's right end cap: a full-height square flush with
2816 the edge, so its centre falls exactly on the cap's centre (half the chip's
2817 height in from the right), whatever size the chip is. The chip's own right
2818 padding is dropped when a closer is present -- the square supplies it. */
2819.tag-x {
2820 position: absolute;
2821 top: 0;
2822 right: 0;
2823 bottom: 0;
2824 aspect-ratio: 1;
2825 display: flex;
2826 align-items: center;
2827 justify-content: center;
2828 /* The flex centring centres the glyph's TEXT BOX, but the × ink rides
2829 between baseline and x-height and hugs the left of its advance --
2830 measured 0.5px low and 0.22px left at 13px. This puts the INK at the
2831 exact centre of the pill's right end cap, in em so it scales. */
2832 transform: translate(0.017em, -0.038em);
2833 background: none;
2834 border: none;
2835 color: inherit;
2836 font-size: var(--fs-sm);
2837 line-height: 1;
2838 padding: 0;
2839 opacity: 0.65;
2840 cursor: pointer;
2841}
2842.tag-x:hover { opacity: 1; }
2843/* The pool chip's closer deletes the TAG, so it reads as the danger it is. Its ×
2844 is drawn here rather than set as the button's text: the chip's textContent is
2845 the tag name, and everything that reads a chip reads that. */
2846.tag-kill::after { content: '\00d7'; }
2847.tag-kill:hover { color: var(--danger); opacity: 1; }
2848.tag-chip:has(.tag-x) { position: relative; padding-right: 2em; }
2849.tag-editor .tag-chip:has(.tag-x) { padding-right: 2em; }
2850
2851/* A tag the rail is currently filtering ON, seen on the Diamond boxes that
2852 survived it. One neutral chip, the same in every palette and for every tag:
2853 the hue says WHICH tag, so it cannot also say which tag is working. A ring in
2854 the tag's own colour said it too quietly -- a 1px outline one hue-step from
2855 the chip it surrounds is not a state a rail can be read at a glance.
2856
2857 Deliberately NOT a box-shadow. A ring drawn outside the border box takes no
2858 layout space, so the pool and the active row -- both `overflow-y: auto` and
2859 both sized to the exact height of one chip row -- sliced it off top and
2860 bottom. That is the clipping in the report: a scroll box cutting ink that was
2861 never in its layout. Ink that has to survive belongs inside the border box.
2862
2863 The selector carries `[data-ink]` because the per-ink chip rules above
2864 are three classes deep; a bare `.tag-inc` loses to them on every theme but
2865 the default, and the whole point of this state is that it does not vary. */
2866:root[data-ink] .tag-chip.tag-inc {
2867 background: var(--tag-on-bg);
2868 border-color: var(--tag-on-bg);
2869 color: var(--tag-on-fg);
2870}
2871:root[data-ink] button.tag-chip.tag-inc:hover {
2872 background: var(--tag-on-hover);
2873 border-color: var(--tag-on-hover);
2874}
2875
2876/* A tag the rail is REFUSING. The negation sign is drawn here rather than set as
2877 the button's text -- a chip's textContent is the tag name, and the rail, the
2878 filter and the search all read it -- and the fill comes off with it, so a
2879 refused tag is told from a wanted one across the rail rather than by reading
2880 it. Same hue, because it is the same tag. */
2881.tag-no::before { content: '\00ac'; font-weight: 700; }
2882.tag-no { background: transparent; border-style: dashed; color: hsl(var(--tag-h, 0) 30% 62%); }
2883button.tag-no:hover { background: hsl(var(--tag-h, 0) 30% 16%); border-color: hsl(var(--tag-h, 0) 40% 48%); }
2884:root[data-ink="dark"] .tag-no { background: transparent; color: hsl(var(--tag-h, 0) 35% 40%); }
2885:root[data-ink="dark"] button.tag-no:hover { background: hsl(var(--tag-h, 0) 60% 96%); }
2886
2887/* All / Any: how two or more wanted tags combine. Both words are shown with one
2888 lit rather than a single label that flips, because a lone word cannot say
2889 whether it is naming the state or the action. It appears only when there are
2890 two tags for it to combine. */
2891.tag-mode { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 20px; overflow: hidden; }
2892.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; }
2893.tag-mode-btn:hover { color: var(--text-primary); }
2894.tag-mode-btn.on { background: var(--accent); color: var(--bg-primary); }
2895
2896/* Put the whole filter down. Deliberately not a chip: it is not a tag. */
2897.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; }
2898.tag-clear-all:hover { color: var(--text-primary); }
2899
2900/* The tag editor, reached from the crystal bar. */
2901.tag-editor { display: flex; flex-direction: column; gap: 10px; max-width: 460px; }
2902.tag-note { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }
2903.tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-height: 20px; }
2904.tag-none { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }
2905.tag-add { display: flex; gap: 6px; }
2906/* No `max-width`. `flex: 1` already sizes it against the "+ Add" beside it, and
2907 the cap on top of that stopped the row 116px short of a 376px card while every
2908 chip row above it ran the full width — one row in the editor that did not
2909 reach the edge the rest reach. */
2910.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; }
2911.tag-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
2912.tag-input::placeholder { color: var(--text-muted); }
2913/* The two boxes: what is on the Diamond, and the pool everything comes from.
2914 The add input sits inside the pool's box, because typing mints into it. */
2915.tag-box {
2916 border: 1px solid var(--border);
2917 border-radius: var(--radius-sm);
2918 background: var(--bg-secondary);
2919 padding: 8px 10px 10px;
2920 display: flex;
2921 flex-direction: column;
2922 gap: 8px;
2923}
2924.tag-box-label { font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
2925.tag-editor .tag-chip { font-size: var(--fs-xs); padding: 2px 9px; }
2926
2927
2928/* ── Credits ─────────────────────────────────────────────────────
2929 Buying credits is a plain one-time purchase: four amounts, no plans, no
2930 auto-renew. The balance sits beside the spend figures, where the user
2931 already looks for what things cost. */
2932.credits-balance { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text-primary); margin-bottom: 10px; }
2933/* Inserted between the balance and the pack buttons; without its own bottom
2934 margin it sat flush on the packs row. */
2935#credits-see-spend { margin: 2px 0 12px; }
2936.credits-packs { display: flex; gap: 8px; }
2937/* `.settings-section button` is accent-filled, which would make four money
2938 buttons shout in red. Money is stated, not sold: quiet by default, and the
2939 accent kept for hover, where it means "this is the one you are choosing". */
2940.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); }
2941.settings-section .credit-pack:hover { background: var(--bg-hover); border-color: var(--accent); color: var(--text-primary); }
2942
2943/* ── Pro, at the top of Credits ───────────────────────────────────
2944 The one deliberate sell in the drawer: owning Daimond. It gets a bordered
2945 card so it reads as an offer rather than a note, and its button IS accent
2946 filled -- unlike the quiet money buttons below, this is the action the panel
2947 is steering toward, and there is only one of it. Owned, it becomes a calm
2948 confirmation with no button at all. */
2949.pro-block { margin-bottom: 14px; }
2950.pro-block:empty { margin: 0; }
2951.pro-offer, .pro-owned { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; background: var(--bg-secondary); }
2952.pro-offer p { margin: 0 0 8px; font-size: var(--fs-base); color: var(--text-primary); line-height: 1.45; }
2953.pro-offer .pro-fine { font-size: var(--fs-sm); color: var(--text-secondary); }
2954.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; }
2955.pro-buy:hover:not(:disabled) { filter: brightness(1.08); }
2956.pro-buy:disabled { opacity: 0.6; cursor: default; }
2957.pro-err { margin-top: 6px; font-size: var(--fs-sm); color: var(--danger, #c0392b); }
2958.pro-owned { color: var(--text-primary); font-size: var(--fs-base); line-height: 1.45; }
2959
2960/* ── Credits, in the Models panel ─────────────────────────────────
2961 Credits are a provider row like any other, with one difference the user must
2962 see before they pick and not after: this row spends money they are holding
2963 HERE, and every other row spends money they hold with somebody else. So the
2964 row is tinted, its balance sits on its name, and each of its models carries
2965 the word. The rest stay unmarked, which is how a reader knows they are not
2966 this — except where two rows serve one model name, where both say whose is
2967 whose, because there the name alone cannot. Nothing shouts: the accent is a
2968 quiet edge, not a banner. Money is stated, not sold. */
2969/* No per-theme rules below: every colour is a variable, so the tint follows the
2970 theme the way the rest of the panel does, and a fourth theme needs nothing
2971 here. Specificity, not order, is what beats models.css's plain head. */
2972.models-prov.paid { border-color: var(--accent-soft); }
2973.models-prov.paid .models-prov-head { background: var(--accent-soft); }
2974.models-prov.paid .models-prov-name { color: var(--accent-text); }
2975
2976/* Name, balance and host are three units on one line that cannot hold three:
2977 the rail is 220px, and this row must survive it. So they wrap as WHOLES --
2978 the break falls between facts, never through one. "$8.40 / left" puts a
2979 number on one line and its meaning on the next, and "Daimond / credits"
2980 hyphenates the product. */
2981.models-prov .models-prov-name { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 7px; }
2982.models-nm { white-space: nowrap; }
2983/* A balance is a fact about the row, not a status, so it rides on the name. */
2984.models-bal { font-family: var(--font-mono); font-weight: 400; font-size: var(--fs-2xs); color: var(--text-primary); white-space: nowrap; }
2985/* Whose machine actually runs it. Subdued, and never the headline: the user
2986 bought Daimond credits, not an OpenRouter account -- but they are still owed
2987 the name of the company their words are sent to. */
2988.models-via { font-weight: 400; font-size: var(--fs-3xs); color: var(--text-muted); white-space: nowrap; }
2989
2990/* The economy a model belongs to, on the model. `credits` draws down the
2991 balance; `your key` marks a model whose name also appears on another row, so
2992 the pair is never a coin toss over who gets paid. */
2993.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); }
2994.models-econ.paid { border-color: var(--accent-soft); color: var(--accent-text); background: var(--accent-soft); }
2995.models-model.on .models-econ.paid { border-color: var(--accent); }
2996
2997/* A model row and the button that copies its id. The row stays the button it
2998 was -- clicking it stars the model as the default -- and the copy sits beside
2999 it rather than in it, since a button cannot hold another one.
3000 `align-items: flex-start` for the same reason `.models-model` uses it: an id
3001 is `word-break: break-all` in a column about 90px wide once the star and the
3002 chips have taken their share of the rail, so two and three line rows are
3003 normal here and the copy belongs on the id's first line. */
3004.models-modelrow { display: flex; align-items: flex-start; gap: 2px; }
3005.models-modelrow > .models-model { flex: 1 1 auto; min-width: 0; }
3006.models-modelrow > .copy-id { align-self: flex-start; margin-top: 3px; }
3007.models-modelrow:hover .copy-id { opacity: 1; }
3008
3009/* The gateway's own words about why it cannot sell inference. It writes better
3010 advice than this panel can ("bring your own model key to keep working"), so
3011 it is shown rather than flattened into a one-word state. */
3012.models-why { padding: 6px 8px 2px; font-size: var(--fs-2xs); line-height: 1.5; color: var(--text-secondary); }
3013.settings-section .credit-pack:disabled { opacity: 0.5; }
3014/* A pack in a non-USD display currency: the round local figure leads, the
3015 exact billed dollars recede beside it. */
3016.credit-pack .pack-local { font-weight: 600; }
3017.credit-pack .pack-billed { opacity: 0.72; font-size: var(--fs-2xs); }
3018/* The billing sentence under the pack grid spans the full row of the flex
3019 container it sits in. */
3020.credits-packs .pack-note {
3021 flex: 1 1 100%;
3022 font-size: var(--fs-2xs);
3023 color: var(--text-muted);
3024 line-height: 1.45;
3025 margin-top: 2px;
3026}
3027/* The billing note after the Pro buy button breathes like the fine print
3028 above it. */
3029.pro-offer .pro-fine + .pro-fine, .pro-offer button + .pro-fine { margin-top: 8px; }
3030/* Credits are a BALANCE, not a spend period, so they take their own line rather
3031 than squeezing in as a fourth column and clipping the rail. */
3032.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); }
3033.credit-cell .spend-amt { color: var(--text-primary); }
3034
3035/* A compiled document: a stage panel, wide enough to actually read. */
3036.doc-name { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; }
3037
3038
3039/* ── Workspace: organise it ──────────────────────────────────────
3040 The tree was read/write only: no rename, no move, no new folder, no
3041 upload, and no way to find anything once it grew past a screenful. */
3042.files-filter { padding: 6px 8px 0; }
3043.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); }
3044.files-filter-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
3045.files-filter-input::placeholder { color: var(--text-muted); }
3046.files-ren { color: var(--text-secondary); }
3047.files-ren:hover { color: var(--accent); }
3048
3049/* The standing-instructions chip: quiet, but visible, so the user knows the
3050 rules are in force and can open them. */
3051.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; }
3052.instructions-chip:hover { color: var(--accent); border-color: var(--accent); }
3053
3054/* A readable multi-line dialog body: a version preview that collapses its
3055 newlines into one run-on line cannot be read, which defeats the History
3056 screen entirely. */
3057.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; }
3058
3059/* N5: the modal's scrollbar rendered as a bright light track on the dark card —
3060 on the first screen a new user sees. */
3061.modal-card::-webkit-scrollbar, .dlg-pre::-webkit-scrollbar { width: 10px; }
3062.modal-card::-webkit-scrollbar-track, .dlg-pre::-webkit-scrollbar-track { background: transparent; }
3063.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; }
3064.modal-card::-webkit-scrollbar-thumb:hover, .dlg-pre::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: content-box; }
3065
3066
3067/* ── The three zones ─────────────────────────────────────────────
3068 Rail (left, permanent) | Stage (middle, exclusive) | Dock (right, tagged).
3069 The stage seats as many panels as its width will carry, up to four, so you
3070 never have to leave the conversation to do a thing: the message, the page,
3071 the document and the pages it was typeset into — each opens BESIDE the
3072 daimon, not over it. How many is derived from the width; see `stageMax` in
3073 daimond.js, and the measurement beside `MIN_W`. */
3074.stage { display: flex; flex: 1; min-width: 0; min-height: 0; gap: 0; }
3075/* `min-width: 0` so a seat is the width the engine gave it. A flex item's
3076 automatic minimum is its content's, so without this a seat asked for 380px
3077 could quietly take more and push the seat beside it off the stage. */
3078.stage > .panel { min-height: 0; min-width: 0; }
3079.dock { display: flex; gap: 8px; min-width: 0; min-height: 0; flex: none; }
3080.pcol { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0; flex: 1; }
3081.pcol > .panel { flex: 1; min-height: 0; width: auto !important; }
3082.pcol:empty { display: none; }
3083/* The divider between two panels stacked in a column: the horizontal twin of
3084 .phandle. It STANDS IN FOR the column's gap rather than adding to it — a
3085 handle placed between panels that were already 8px apart put 26px of nothing
3086 between two cards — so a stacked column gives its gap up to the handles. */
3087.pcol.stacked { gap: 0; }
3088.hstack { height: 16px; flex: none; cursor: row-resize; position: relative; touch-action: none; }
3089/* STANDING, AND THE FULL WIDTH, exactly as the rail's own `.rhandle` stands
3090 between Chats and Diamonds.
3091
3092 It used to be transparent at rest and inset a tenth either end, on the
3093 reading that the panels either side "carry their own borders, so a line at
3094 rest would be a third edge in the gap". They do not. A border and a radius
3095 are given to `.ai, .web, .doc, .msg, .compose, .tools` -- the STAGE's cards.
3096 Everything that seats in the dock (`.rail, .agents, .work, .mail, .spend`) is
3097 a bare column with no edge of its own, so this line was not a third edge, it
3098 was the only one -- and a divider that appears under the pointer is a
3099 divider nobody finds, since finding it is what the pointer was looking for.
3100 Reported as: several panels in one dock column could not be told apart.
3101
3102 The rail's rule is copied rather than adapted, so the two dividers are one
3103 idea: `--border` at rest, `--accent` under the pointer or in a drag. Only the
3104 offset differs, and only because this handle is 16px tall against the rail's
3105 10px. */
3106.hstack::after { content: ""; position: absolute; left: 0; right: 0; top: 7px; height: 2px; border-radius: 2px; background: var(--border); transition: background 0.15s; }
3107.hstack:hover::after, .hstack.dragging::after { background: var(--accent); }
3108
3109/* ── The Web panel ───────────────────────────────────────────────
3110 A page can always be SHOWN. It can be operated only where a driver can
3111 reach it: a same-origin page we serve ourselves, or a real tab under
3112 Daimond Hands. A cross-origin iframe is opaque to us — that is the
3113 same-origin policy, not a gap to be worked around. */
3114/* A COLUMN, so a standing strip can sit above the picture rather than beside
3115 it. It held one flexible child until `.web-live` arrived, so the direction
3116 made no difference before and makes one now; the three things that cover this
3117 body -- the note, the blindfold and the read-only text copy -- are all
3118 `position: absolute; inset: 0` and are unaffected either way. */
3119.web-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
3120/* Why there are two of them: a real browser tab, and this panel's picture of
3121 it. Dressed as `.web-readonly` because it is the same kind of sentence --
3122 a standing fact about what you are looking at, said quietly and never
3123 dismissed. */
3124.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); }
3125.web-live[hidden] { display: none; }
3126.web-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
3127.web-mirror { flex: 1; min-height: 0; width: 100%; object-fit: contain; object-position: top; background: #fff; }
3128.web-url { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); }
3129.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; }
3130.web-note.on { display: flex; }
3131.web-note b { color: var(--text-primary); }
3132.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); }
3133
3134/* Who is driving, said in the header rather than buried. */
3135.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; }
3136.web-mode.agent { color: var(--accent-text); background: var(--accent-soft); border-color: var(--accent); }
3137.web-mode.user { color: var(--ok); background: var(--ok-bg); border-color: var(--ok); }
3138
3139/* The blindfold. Not a promise to look away — nothing is sent. The panel says
3140 so, because a security property the user cannot see is one they cannot
3141 trust. */
3142.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); }
3143.web-blind-eye { font-size: var(--fs-6xl); color: var(--ok); opacity: 0.85; }
3144.web-blind-title { font-size: var(--fs-xl); font-weight: 600; color: var(--text-primary); }
3145.web-blind-note { font-size: var(--fs-sm); color: var(--text-secondary); line-height: 1.6; max-width: 42ch; }
3146.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); }
3147.web-takeover:hover { background: var(--accent-hover); }
3148
3149/* A stage panel's header reuses the AI panel's chrome.
3150
3151 And it WRAPS. `.chead-right` is `flex: none` and only `.panel.ai .ctitle`
3152 carried `min-width: 0`, so in a narrow stage seat these three heads squeezed
3153 the title to nothing and then pushed the button row out of the panel: at a
3154 205px seat the head overflowed by 23px and the close button sat at 205..227
3155 — entirely outside the panel it closes, so the Web panel could not be shut
3156 from its own header. Measured at three widths; 300 and 250 were still inside.
3157 A row that drops is a row you can still press. */
3158.web .chead, .doc .chead, .msg .chead { gap: 8px; flex-wrap: wrap; row-gap: 4px; }
3159/* AND THE BASIS IS ZERO, NOT `auto`, WHICH IS WHAT MAKES THE ROW ABOVE A LAST
3160 RESORT RATHER THAN THE USUAL CASE. A wrapping flex container breaks its lines
3161 on each item's flex BASE size, before any shrinking -- so with `auto` a title
3162 whose text is 800px wide claimed a line of its own at any panel width, and
3163 pushed the button row onto a third. Measured 2026-08-28 in a 413px Web panel:
3164 an ordinary URL took the head from 53px to 102px and left the closer at 229px
3165 from the panel's right edge and 65px down, which is the fault reported as "the
3166 cross should be at a fixed position in the top right corner". With a basis of
3167 zero the title never forces a break, ellipsises as it was always meant to, and
3168 the row still drops at the 205px seat the comment above measured. */
3169.web .ctitle, .doc .ctitle, .msg .ctitle { flex: 1 1 0; min-width: 0; }
3170/* The Doc panel holds the file as characters. The Preview panel holds what it
3171 looks like: a PDF in `.doc-embed`, anything else the viewer draws in
3172 `#pv-view`, and the live typeset pages in front of both. Each fills its
3173 panel; whichever is showing, the others are display:none. */
3174.doc-view { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
3175.doc-embed { flex: 1; width: 100%; min-height: 0; border: 0; background: var(--bg-tertiary); }
3176/* Main needs room to be Main — but only where there is room to give. On a phone
3177 one panel fills the screen, so a desktop minimum would just force an overflow. */
3178/* The stage needs room to be the stage — but only where there is room to give.
3179 On a phone one panel fills the screen, so a desktop minimum would just force
3180 an overflow. */
3181@media (min-width: 761px) { #stage { min-width: 420px; } }
3182@media (max-width: 760px) { #stage { min-width: 0; } }
3183
3184
3185body.resizing { cursor: col-resize; user-select: none; }
3186
3187/* The theme control is an icon button like the rest now, not a pair of emoji
3188 sitting among a hand-drawn SVG set. */
3189.theme-toggle .ic { width: 18px; height: 18px; }
3190
3191/* A page the gateway read for us. It is a COPY, not a session on the site — the
3192 badge says so, because a user who mistakes it for the live page might try to
3193 sign in to it, and a page served from our origin is a page we can read. */
3194.web-text { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--bg-secondary); overflow: hidden; }
3195.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); }
3196.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); }
3197
3198
3199/* The permission chip: a word in the chat header saying what Daimond does
3200 without asking. The WORD carries the state — the dot and the accent only
3201 help the eye land, so nothing is lost on a palette or to a reader who
3202 cannot tell the colours apart. */
3203.mode-chip { display: inline-flex; align-items: center; gap: 5px; }
3204.mode-chip-dot {
3205 flex: none;
3206 width: 6px; height: 6px;
3207 border-radius: 50%;
3208 background: var(--text-muted);
3209 /* Measured, not nudged. dev/shot_permchip.mjs reads the font's own metrics and
3210 asserts this number; dev/shot_permchip_ladder.mjs is the 8x crop it was
3211 chosen from.
3212
3213 Two things the line box gets wrong. Its centre sits 1px above the band the
3214 letters occupy, because the ascent carries accent space nothing in these
3215 labels uses. And the three labels disagree with each other: "Guarded" has an
3216 ascender and no descender, "Bypass" has both, and their ink centres are 2px
3217 apart — so no single offset centres the dot on all three.
3218
3219 This is the midpoint of those two extremes, which is the offset whose WORST
3220 case is smallest: 1px out on either, rather than 2px out on one. At 8x, +1px
3221 reads low against "Guarded" and -0.5px reads high against "Bypass"; this
3222 reads level against all three. */
3223 transform: translateY(0.5px);
3224}
3225.mode-chip[data-mode="ask"] .mode-chip-dot { background: var(--ok); }
3226.mode-chip[data-mode="bypass"] .mode-chip-dot { background: var(--accent); }
3227/* This chat has lost the network, which outranks the rung's own tint on the one
3228 mark the chip has. No conflict with the bypass accent above: bypass withholds
3229 nothing, so a bypass chip is never cut. LAST, so it wins on order.
3230
3231 The dot is the right place for it because the dot was never carrying anything
3232 -- the rung is the word beside it, and index.html says so: "The word IS the
3233 state; the accent is only so the eye lands." The colour alone is not the
3234 signal; `handmode.js` puts "no network" into the chip's aria-label, and the
3235 popover says it in a sentence. */
3236/* THE PILL'S OWN EDGE, not only the dot. Tried the dot alone first, twice: amber
3237 against grey is a 6px difference somebody has to already know about, and an
3238 open ring inside 6px leaves a 2px hole that reads at real size as a slightly
3239 lighter dot rather than as a ring. Both were looked at, blown up 8x, and
3240 rejected there -- the sweep would have passed either.
3241
3242 The edge is 60px of line instead of 6px of fill, it costs no width on a phone,
3243 and it does not disturb the ladder's own colours. The dot follows it so the two
3244 marks are one signal. */
3245.mode-chip.net-cut { border-color: var(--warn); }
3246.mode-chip.net-cut .mode-chip-dot { background: var(--warn); }
3247
3248/* The picker. `.pop` already carries the surface, the shadow and the scroll. */
3249.mode-pop .mode-row {
3250 display: flex;
3251 align-items: flex-start;
3252 gap: 8px;
3253 padding: 7px 6px;
3254 border-radius: var(--radius-sm);
3255 cursor: pointer;
3256}
3257.mode-pop .mode-row:hover { background: var(--bg-hover); }
3258.mode-pop .mode-row.on { background: var(--accent-soft); }
3259.mode-pop .mode-row input { margin-top: 3px; flex: none; accent-color: var(--accent); }
3260.mode-pop .mode-row-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
3261.mode-pop .mode-row-name { color: var(--text-primary); font-size: var(--fs-xs); }
3262.mode-pop .mode-row-blurb { color: var(--text-secondary); font-size: var(--fs-2xs); line-height: 1.4; }
3263.mode-pop .mode-row:focus-within { outline: 2px solid var(--accent); outline-offset: -2px; }
3264/* This chat's own network, below the rungs. `.pop-head` already draws the rule
3265 that separates the two scopes, so the note under it must not draw a second one
3266 three pixels lower. */
3267.mode-pop .net-now { border-top: none; margin-top: 0; padding: 2px 6px 0; color: var(--text-secondary); }
3268/* The three standing choices, one row. Chips and not a second radio ladder: the
3269 rungs above are a policy with a reason each, this is one answer with three
3270 values, and giving it the ladder's shape would say the two were the same kind
3271 of thing. `wrap` because the longest set -- French -- does not fit 380px. */
3272.mode-pop .net-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 6px 2px; }
3273.mode-pop .net-opt { flex: none; }
3274
3275/* ── Triggered actions, in a Diamond's settings dialog ───────────
3276 One row per action: its light, what it is in words, and the way in.
3277 Notes2 warns about clutter here by name — "To avoid clutter, Instruction and
3278 Context should just show an edit button and a copy button" — so the two long
3279 texts are behind buttons and never inline. Eight actions with their
3280 instructions on the page would be a dialog nobody reads. */
3281.trig-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
3282.trig-row { display: flex; align-items: center; gap: 6px; padding: 4px 0; }
3283.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; }
3284/* Where a control would be, on the one action that has none: `prompted` is the
3285 daimon answering you, and `/self` is already its control. 59px is the group's
3286 own width -- 20 for each verb, 13 for the lamp, 3 of gap between them -- so
3287 the name beside it starts on the same edge as every other row's. */
3288/* `--border-strong`, not `--border`: a divider separates two things that are
3289 already there, and a control's edge has to say the control IS there. Phase H
3290 copied this rule from a box rather than from a button. */
3291.trig-btn {
3292 flex: none; width: 26px; height: 26px; border-radius: var(--radius-sm);
3293 background: var(--bg-tertiary); border: 1px solid var(--border-strong);
3294 color: var(--text-secondary); font-size: var(--fs-sm); line-height: 1;
3295}
3296.trig-btn:hover { border-color: var(--accent); color: var(--text-primary); }
3297/* The chooser: the light, the pulldown naming every action, and the way to
3298 remove the one showing. One row, and it does not change shape when a
3299 different action is chosen -- the light is the control reached for most and
3300 it should not move under the pointer. */
3301.trig-pick { display: flex; align-items: center; gap: 6px; padding: 2px 0 6px; }
3302.trig-choose { flex: 1 1 auto; min-width: 0; }
3303/* The chosen action's own settings, set in from the chooser so it reads as
3304 belonging to it rather than as the next thing down the dialog. */
3305.trig-panel {
3306 display: flex; flex-direction: column; gap: 8px;
3307 padding: 8px 0 2px 10px;
3308 border-left: 2px solid var(--border);
3309}
3310/* A long text: what it says, then Edit and Copy. The gist is what stops two
3311 of these rows being indistinguishable from each other. */
3312.trig-text-row { display: flex; align-items: center; gap: 6px; }
3313.trig-text-row .trig-label { flex: none; }
3314.trig-gist {
3315 flex: 1 1 auto; min-width: 0;
3316 font-size: var(--fs-sm); color: var(--text-secondary);
3317 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3318}
3319.trig-gist.empty { color: var(--text-muted); font-style: italic; }
3320.trig-add { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
3321.trig-add .tile-model { flex: 1 1 auto; min-width: 0; }
3322.trig-edit { display: flex; flex-direction: column; gap: 10px; }
3323.trig-field { display: flex; flex-direction: column; gap: 4px; }
3324.trig-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
3325.trig-input, .trig-area {
3326 background: var(--bg-tertiary); border: 1px solid var(--border-strong);
3327 border-radius: var(--radius-sm); color: var(--text-primary);
3328 padding: 6px 8px; font-size: var(--fs-sm); width: 100%;
3329}
3330.trig-area { font-family: var(--font-mono); resize: vertical; min-height: 64px; }
3331.trig-input:focus, .trig-area:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
3332
3333/* ── Pending ─────────────────────────────────────────────────────
3334 A card per thing waiting on you. The headline is a button because it
3335 EXPANDS: a panel of paragraphs cannot be scanned and a panel of one-liners
3336 cannot be judged, so it is both and the reader chooses which. */
3337.pending-actions { display: flex; align-items: center; gap: 6px; }
3338.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; }
3339.pending-list { display: flex; flex-direction: column; gap: 6px; padding: 8px; overflow-y: auto; }
3340.pend-card {
3341 background: var(--bg-tertiary); border: 1px solid var(--border);
3342 border-left-width: 3px; border-radius: var(--radius-sm); padding: 8px 10px;
3343 display: flex; flex-direction: column; gap: 6px;
3344}
3345/* Priority as an EDGE, not a fill: the card stays readable and the column of
3346 edges down the left is what makes a list scannable at a glance. */
3347.pend-card.prio-high { border-left-color: var(--danger); }
3348.pend-card.prio-normal { border-left-color: var(--accent); }
3349.pend-card.prio-low { border-left-color: var(--border-strong); }
3350.pend-head { display: flex; align-items: center; gap: 6px; font-size: var(--fs-2xs); color: var(--text-muted); }
3351.pend-prio { background: transparent; border: 1px solid var(--border); border-radius: 20px; color: var(--text-muted); font-size: var(--fs-3xs); padding: 0 4px; }
3352.pend-diamond { font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3353.pend-when { margin-left: auto; flex: none; }
3354.pend-line {
3355 background: none; border: 0; padding: 0; text-align: left;
3356 color: var(--text-primary); font-size: var(--fs-sm); line-height: 1.4;
3357}
3358.pend-line:hover { color: var(--accent-text); }
3359.pend-line:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
3360.pend-detail { font-size: var(--fs-xs); color: var(--text-secondary); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
3361.pend-files { display: flex; flex-wrap: wrap; gap: 4px; }
3362.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; }
3363.pend-file:hover { color: var(--text-primary); border-color: var(--accent); }
3364.pend-acts { display: flex; gap: 6px; }
3365.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); }
3366.pend-act:hover { color: var(--text-primary); }
3367.pend-go:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-text); }
3368.pend-no:hover { border-color: var(--danger); color: var(--danger); }
3369.pend-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
3370
3371/* ── The `/` menu ────────────────────────────────────────────────
3372 Skills have worked since seq 65 and nothing told anyone they existed. A `/`
3373 alone in an empty composer lists them. Above the box, because the composer
3374 sits at the foot of the panel and a menu below it would be off-screen. */
3375.skill-menu {
3376 position: fixed; z-index: 60; max-height: 40vh; overflow-y: auto;
3377 background: var(--bg-secondary); border: 1px solid var(--border-strong);
3378 border-radius: var(--radius-sm); box-shadow: 0 8px 24px rgba(0,0,0,0.28);
3379 display: flex; flex-direction: column; padding: 4px;
3380}
3381.skill-item {
3382 background: none; border: 0; text-align: left; padding: 6px 10px;
3383 border-radius: var(--radius-sm); color: var(--text-primary);
3384 font-family: var(--font-mono); font-size: var(--fs-sm);
3385}
3386.skill-item:hover { background: var(--accent-soft); color: var(--accent-text); }
3387.skill-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
3388.skill-none { padding: 8px 10px; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }
3389/* An empty Diamond chat says what it is, rather than looking like a thread that
3390 failed to load. Centred and quiet: it is a signpost, not a message. */
3391.chat-msg-empty {
3392 color: var(--text-muted); font-size: var(--fs-sm); line-height: 1.6;
3393 text-align: center; max-width: 34rem; margin: 12vh auto 0; padding: 0 16px;
3394}
3395
3396/* ── The trail on the lock screen ────────────────────────────────────────────
3397 Only on screen when the app has booted three times in ninety seconds, which
3398 is not something a person does. It exists because a loop on an iPhone could
3399 be described but never seen: reading a browser console on iOS needs a Mac
3400 attached, so three reports produced two wrong diagnoses and no evidence. */
3401.id-trail {
3402 margin-top: 1rem;
3403 padding-top: 0.75rem;
3404 border-top: 1px solid var(--border);
3405 text-align: left;
3406}
3407.id-trail-lead {
3408 margin: 0 0 0.5rem;
3409 font-size: var(--fs-xs);
3410 color: var(--danger);
3411}
3412/* Monospace and scrollable: it is a log, and a log that wraps into prose
3413 cannot be read at a glance on a phone. */
3414.id-trail-text {
3415 margin: 0;
3416 max-height: 34vh;
3417 overflow: auto;
3418 padding: 0.5rem 0.6rem;
3419 background: var(--bg-tertiary);
3420 border: 1px solid var(--border);
3421 border-radius: var(--radius-sm);
3422 font-family: var(--font-mono);
3423 font-size: var(--fs-3xs-2xs);
3424 line-height: 1.5;
3425 color: var(--text-secondary);
3426 white-space: pre;
3427 -webkit-overflow-scrolling: touch;
3428}
3429.id-trail-acts { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
3430.id-trail-btn {
3431 flex: 1;
3432 padding: 0.4rem 0.6rem;
3433 font-size: var(--fs-xs);
3434 color: var(--text-secondary);
3435 background: var(--bg-tertiary);
3436 border: 1px solid var(--border-strong);
3437 border-radius: var(--radius-sm);
3438}
3439.id-trail-btn:hover { color: var(--text-primary); border-color: var(--accent); }
3440.id-trail-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
3441
3442/* ── An answer folded in two ──────────────────────────────────────────
3443 `say` returns a summary and a detail. The summary reads as an ordinary
3444 answer, because that is what it is; the detail sits behind a control that
3445 says how big it is, since the size is the whole of what a reader needs to
3446 decide whether to open it.
3447
3448 The left edge is what marks it as folded rather than merely short. Without
3449 it, a two-sentence answer with a small control under it looks like an answer
3450 that got cut off. */
3451.chat-msg.said { border-left: 2px solid var(--accent-soft); padding-left: 10px; }
3452.said-more {
3453 display: block;
3454 margin: 8px 0 0;
3455 padding: 4px 0;
3456 background: none;
3457 border: none;
3458 color: var(--text-muted);
3459 font-size: var(--fs-sm);
3460 text-align: left;
3461 cursor: pointer;
3462}
3463.said-more:hover { color: var(--accent-text); }
3464/* Set apart from the summary above it, or an opened fold reads as one long
3465 answer and the reader loses which part they chose to see. */
3466.said-detail {
3467 margin-top: 8px;
3468 padding-top: 8px;
3469 border-top: 1px solid var(--border);
3470}
3471
3472/* ── A question the model put, answered with one tap ──────────────────
3473 IT MUST NOT LOOK LIKE A PERMISSION PROMPT. `.modal.dlg` is what asks
3474 "may I do this": centred, over a scrim, danger-coloured, and it takes
3475 the screen. This is the opposite question -- "which of these do you
3476 want" -- so it sits IN the thread, in the accent, takes no focus and
3477 blocks nothing. The two mean opposite things and a reader has to be
3478 able to tell them apart at a glance. */
3479.ask-card {
3480 margin: 12px 0;
3481 padding: 12px 14px;
3482 border: 1px solid var(--accent);
3483 border-left-width: 3px;
3484 border-radius: var(--radius);
3485 background: var(--bg-secondary);
3486}
3487.ask-card.answered { border-color: var(--border-2); background: var(--bg-primary); }
3488.ask-count {
3489 font-size: var(--fs-2xs);
3490 color: var(--text-muted);
3491 text-transform: uppercase;
3492 letter-spacing: 0.06em;
3493 margin-bottom: 4px;
3494}
3495.ask-q {
3496 font-size: var(--fs-lg);
3497 color: var(--text-primary);
3498 line-height: 1.4;
3499 margin-bottom: 10px;
3500}
3501.ask-opts { display: flex; flex-direction: column; gap: 6px; }
3502/* One tap answers, so the whole option is the target: the label, the badge
3503 and what it means are all inside the button. A row where only a small
3504 word was clickable would be a decision answered by aiming. */
3505.ask-opt {
3506 display: block;
3507 width: 100%;
3508 text-align: left;
3509 padding: 8px 10px;
3510 border: 1px solid var(--border-2);
3511 border-radius: var(--radius-sm);
3512 background: var(--bg-tertiary);
3513 color: var(--text-primary);
3514 font: inherit;
3515 cursor: pointer;
3516}
3517.ask-opt:hover { background: var(--bg-hover); border-color: var(--border-strong); }
3518.ask-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
3519.ask-opt.recommended { border-color: var(--accent); background: var(--accent-soft); }
3520.ask-label { font-size: var(--fs-base); font-weight: 600; }
3521/* NAMED, not merely first. A recommendation implied by ordering is not one. */
3522.ask-rec {
3523 margin-left: 8px;
3524 padding: 1px 6px;
3525 border-radius: 999px;
3526 background: var(--accent);
3527 color: var(--on-fill);
3528 font-size: var(--fs-3xs);
3529 text-transform: uppercase;
3530 letter-spacing: 0.06em;
3531 vertical-align: middle;
3532}
3533.ask-means {
3534 display: block;
3535 margin-top: 3px;
3536 font-size: var(--fs-sm);
3537 color: var(--text-secondary);
3538 line-height: 1.45;
3539}
3540.ask-why, .ask-silent {
3541 margin-top: 8px;
3542 font-size: var(--fs-sm);
3543 color: var(--text-secondary);
3544 line-height: 1.45;
3545}
3546.ask-silent { color: var(--text-muted); }
3547.ask-other { margin-top: 10px; }
3548.ask-other-open {
3549 padding: 0;
3550 background: none;
3551 border: none;
3552 color: var(--text-muted);
3553 font: inherit;
3554 font-size: var(--fs-sm);
3555 cursor: pointer;
3556}
3557.ask-other-open:hover { color: var(--accent-text); }
3558.ask-other-row { display: flex; gap: 6px; }
3559.ask-other-box {
3560 flex: 1;
3561 min-width: 0;
3562 padding: 6px 8px;
3563 border: 1px solid var(--border-2);
3564 border-radius: var(--radius-sm);
3565 background: var(--bg-primary);
3566 color: var(--text-primary);
3567 font: inherit;
3568 font-size: var(--fs-sm);
3569}
3570.ask-other-go {
3571 padding: 6px 10px;
3572 border: 1px solid var(--accent);
3573 border-radius: var(--radius-sm);
3574 background: var(--accent);
3575 color: var(--on-fill);
3576 cursor: pointer;
3577}
3578/* An answered card keeps its options and stops offering them. What it was
3579 decided AGAINST is the part somebody comes back to a month later. */
3580.ask-opt:disabled { cursor: default; opacity: 0.55; }
3581.ask-opt:disabled:hover { background: var(--bg-tertiary); border-color: var(--border-2); }
3582.ask-opt.chosen {
3583 opacity: 1;
3584 border-color: var(--ok);
3585 background: var(--ok-bg);
3586}
3587.ask-opt.chosen:disabled:hover { background: var(--ok-bg); border-color: var(--ok); }
3588/* The recommendation badge on a card already answered is history, not advice. */
3589.ask-card.answered .ask-rec { background: var(--border-2); color: var(--text-secondary); }
3590.ask-done {
3591 margin-top: 8px;
3592 font-size: var(--fs-sm);
3593 color: var(--ok);
3594}
3595
3596/* ── The Wire: the payload, annotated where it lies ───────────────────
3597 Not a panel beside the thread. The thread IS the accumulated payload,
3598 laid out once, so this turns on the parts of it that are normally
3599 invisible rather than printing a second copy of the parts that are not. */
3600.wire-head {
3601 margin: 0 0 14px;
3602 padding: 10px 12px;
3603 border: 1px solid var(--border);
3604 border-radius: var(--radius);
3605 background: var(--bg-secondary);
3606}
3607.wire-title {
3608 font-size: var(--fs-sm);
3609 color: var(--text-muted);
3610 margin-bottom: 8px;
3611}
3612.wire-band-head {
3613 display: flex;
3614 align-items: baseline;
3615 gap: 8px;
3616 width: 100%;
3617 padding: 5px 0;
3618 background: none;
3619 border: none;
3620 border-top: 1px solid var(--border);
3621 color: var(--text);
3622 font-size: var(--fs-sm);
3623 text-align: left;
3624 cursor: pointer;
3625}
3626.wire-band:first-of-type .wire-band-head { border-top: none; }
3627.wire-band-name { flex: none; }
3628/* WHOSE it is, which is the thing this view exists to say. Muted, because it
3629 is a gloss on the name rather than a second name. */
3630.wire-band-why { flex: 1 1 auto; color: var(--text-muted); font-size: var(--fs-xs); }
3631.wire-band-tok { flex: none; color: var(--text-muted); font-variant-numeric: tabular-nums; }
3632.wire-band-body {
3633 margin: 6px 0 8px;
3634 padding: 8px 10px;
3635 max-height: 24rem;
3636 overflow: auto;
3637 background: var(--bg-tertiary);
3638 border-radius: var(--radius-sm);
3639 font-size: var(--fs-xs);
3640 white-space: pre-wrap;
3641 word-break: break-word;
3642}
3643
3644/* ── A chip that is a picture ─────────────────────────────────────────
3645 The row was six words in a line — Guarded, Concise, Wire, Copy, Steps —
3646 which reads as a sentence you have to parse before you can press
3647 anything. The verbs become drawings and keep their words on hover; the
3648 MODE chip stays a word, because it names a state with three values and a
3649 picture of it would be a guess. */
3650.chip-btn.icon-chip {
3651 display: inline-flex;
3652 align-items: center;
3653 justify-content: center;
3654 /* A FLOOR ON BOTH AXES. Width alone left the height to the row, and at 761-768px it
3655 collapsed under 24px — which `verify_sweep_mobile` reported as a target too small to
3656 press, on three of these, at exactly the widths a tablet uses. A word-shaped chip had
3657 its padding to keep it open; a drawing has nothing but this. */
3658 /* THE ROW'S OWN HEIGHT, measured off its neighbours rather than chosen: the mode chip
3659 and the collapse button are both 26px, and at 30 these sat four pixels taller and
3660 square where the rest of the row is a pill. On screen they read as heavier than the
3661 words beside them, which is the opposite of what a drawing is here for. */
3662 width: 26px;
3663 min-width: 26px;
3664 height: 26px;
3665 min-height: 26px;
3666 padding: 0;
3667}
3668.chip-btn.icon-chip .ic { width: 14px; height: 14px; }
3669
3670/* A WORD AMONG THE DRAWINGS, AT THE DRAWINGS' HEIGHT. One chip in this row keeps
3671 its label -- "Wire", which is not a picture of anything -- and a `.chip-btn`'s
3672 own padding would draw it two pixels taller than the 26px boxes either side of
3673 it, so the row would grow rather than the chip fitting. The height is taken
3674 from `.icon-chip` above, deliberately, so the two cannot drift apart. */
3675.chip-btn.label-chip {
3676 display: inline-flex;
3677 align-items: center;
3678 justify-content: center;
3679 height: 26px;
3680 min-height: 26px;
3681 padding: 0 9px;
3682 line-height: 1;
3683}
3684/* Pressed reads as pressed. Without this an icon toggle has no state at all,
3685 where a word one at least changed weight. */
3686.chip-btn.icon-chip[aria-pressed="true"] {
3687 background: var(--accent-soft);
3688 color: var(--accent-text);
3689 border-color: var(--accent);
3690}
3691
3692/* ── A fold the model wrote itself ────────────────────────────────────
3693 `<details>` now survives the renderer's whitelist, so a model can fold
3694 part of an answer away in the answer itself, with no tool involved. The
3695 class is stamped by the sanitiser (`render.js`) rather than matched on a
3696 bare `details`, because the app draws one of its own -- the release notes'
3697 list of sealed builds, `.rel-builds` -- and that one keeps the browser's
3698 look. A model's fold cannot reach this selector without going through the
3699 sanitiser first.
3700
3701 Drawn as the `say` fold above is drawn, and for the same reasons: a left
3702 edge that says folded rather than merely short, and a rule between the label
3703 and what it opens onto. The disclosure triangle is the browser's own,
3704 inheriting `color` from the summary, since a model's summary text can be
3705 anything and a fold with no affordance but a colour change is a fold nobody
3706 opens.
3707
3708 THE SUMMARY IS NOT A CAPTION AND IS NO LONGER DRAWN AS ONE. It was muted at
3709 `--fs-sm` while the prompt modelled it as "a few words naming what is
3710 inside", and the owner, 2026-08-23, found a real one on his own screen only
3711 after being told where to look: four muted words at 13px, between a reply
3712 above and a reply below, is a caption for something rather than a thing to
3713 read. `FOLD_NOTE` now asks for a sentence or two of what the fold concludes,
3714 so the summary carries the substance of the half that is hidden and has to
3715 be legible at the weight of the answer it belongs to. The left edge and the
3716 triangle are what say it is foldED; the type no longer says it is minor.
3717
3718 The thinking tile below KEEPS the quiet treatment, and the difference is the
3719 reader: that one is the model's own working, not addressed to anybody, and
3720 it must not compete with the reply. It carries BOTH classes -- `md-fold
3721 chat-msg-thinking`, `daimond.js:6782` -- so it inherited this rule and had to
3722 be given the muted size and colour back by name. A comment here saying it was
3723 untouched would have been true of the intent and wrong about the screen. */
3724details.md-fold {
3725 margin: 8px 0;
3726 padding-left: 10px;
3727 border-left: 2px solid var(--accent-soft);
3728}
3729details.md-fold > summary {
3730 padding: 4px 0;
3731 color: var(--text-primary);
3732 font-size: var(--fs-base);
3733 cursor: pointer;
3734}
3735details.md-fold > summary:hover { color: var(--accent-text); }
3736details.md-fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
3737/* Set apart from the label above it, or an opened fold reads as one long
3738 answer and the reader loses which part they chose to see. Only when open:
3739 a closed fold is a single line and a rule under it marks off nothing. */
3740details.md-fold[open] > summary {
3741 margin-bottom: 8px;
3742 padding-bottom: 8px;
3743 border-bottom: 1px solid var(--border);
3744}
3745/* The last paragraph inside carries the bottom margin every `p` gets, which
3746 below a border reads as the fold not having ended. */
3747details.md-fold > :last-child { margin-bottom: 0; }
3748
3749/* The model's own working. Same disclosure as a two-depth answer, quieter: it
3750 is not addressed to the reader, so it must not compete with the reply above
3751 it. Pre-wrap because reasoning arrives with its own line breaks and nothing
3752 parses it into paragraphs. */
3753details.chat-msg-thinking > summary {
3754 font-style: italic;
3755 opacity: 0.72;
3756 /* Taking back what `details.md-fold > summary` now gives every fold. Same
3757 specificity, later in the file, so these win. */
3758 color: var(--text-muted);
3759 font-size: var(--fs-sm);
3760}
3761.chat-thinking-body {
3762 white-space: pre-wrap;
3763 font-size: 0.92em;
3764 opacity: 0.86;
3765}
3766
3767/* ── The same fold, drawn while it is still arriving ──────────────────
3768 A fold that streams keeps ONE `<details>` for the whole of a generation --
3769 rebuilt per frame it would snap shut sixty times a second -- so the chat
3770 redraws its body inside a wrapper rather than the element itself, and the
3771 runs of ordinary answer around it get wrappers of their own. Neither is a
3772 box: no margin, no padding, no rule, so a fold watched arriving and the same
3773 fold after a reload are the same picture.
3774
3775 `> :last-child` above lands on the wrapper rather than on the paragraph it
3776 holds, which is why the rule is repeated one level in. */
3777details.md-fold > .md-fold-body > :last-child { margin-bottom: 0; }
3778.chat-msg-content > .md-seg > :last-child { margin-bottom: 0; }