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; } |
| 2 | html, body { height: 100%; width: 100%; overflow: hidden; } |
| 3 | body { font-family: var(--font); background: var(--bg-primary); color: var(--text-primary); font-size: var(--fs-base); line-height: 1.5; transition: background 0.2s, color 0.2s; -webkit-font-smoothing: antialiased; } |
| 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. */ |
| 10 | body { background-image: var(--body-wash, none); background-attachment: fixed; } |
| 11 | button, input, textarea, select { font-family: inherit; font-size: inherit; color: inherit; } |
| 12 | button { cursor: pointer; } |
| 13 | svg { 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; } |
| 231 | body.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 | } |
| 254 | label.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); } |
| 375 | body.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. */ |
| 1105 | button.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; } |
| 1207 | button.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. */ |
| 1214 | span.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. */ |
| 2613 | body.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%); } |
| 2795 | button.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%); } |
| 2883 | button.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 | |
| 3185 | body.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. */ |
| 3724 | details.md-fold { |
| 3725 | margin: 8px 0; |
| 3726 | padding-left: 10px; |
| 3727 | border-left: 2px solid var(--accent-soft); |
| 3728 | } |
| 3729 | details.md-fold > summary { |
| 3730 | padding: 4px 0; |
| 3731 | color: var(--text-primary); |
| 3732 | font-size: var(--fs-base); |
| 3733 | cursor: pointer; |
| 3734 | } |
| 3735 | details.md-fold > summary:hover { color: var(--accent-text); } |
| 3736 | details.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. */ |
| 3740 | details.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. */ |
| 3747 | details.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. */ |
| 3753 | details.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. */ |
| 3777 | details.md-fold > .md-fold-body > :last-child { margin-bottom: 0; } |
| 3778 | .chat-msg-content > .md-seg > :last-child { margin-bottom: 0; } |