orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1/* Cassette futurism, held to the same rule as dash and search: the texture is
2 structure, not costume. Green because DESIGN.md reserves green for the tools
3 read for minutes at a time, and a conversation is the longest read here.
4
5 The layout is deliberately a familiar chat: history on the left, thread in
6 the middle, composer pinned at the bottom. The costume is in the palette and
7 the chrome, not in where things are, because a chat you have to learn is
8 worse than one that looks like every other chat.
9
10 Every value is measured against the ground with the overlays applied. A
11 multiply blend over the page costs more contrast than any colour choice, so
12 do not darken one of these to soften something without re-measuring. */
13
14:root {
15 --void: #0a0908;
16 --bg: #0e0d0a;
17 --panel: #1e1b16;
18 --panel-2: #29251e;
19 --bar: #201d16;
20 --line: #414e41;
21 --line-hot: #5c725c;
22
23 --text: #ece5d8;
24 --muted: #c6b9a3;
25 --dim: #ab9c86;
26 --faint: #9c8e7a;
27
28 --green: #6b9e78;
29 --green-bright: #7db88c;
30 --green-hot: #95cca2;
31 --green-glow: rgba(107, 158, 120, 0.16);
32
33 --amber: #d4b45a;
34 --terracotta: #c47055;
35
36 --mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
37 --track: 0.14em;
38 --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
39
40 --col: 46rem; /* the reading column for a message */
41 --side: 16.5rem;
42 --barh: 3.1rem;
43}
44
45* { box-sizing: border-box; }
46
47html {
48 color-scheme: dark;
49 scrollbar-color: var(--line-hot) var(--void);
50}
51
52body {
53 margin: 0;
54 background: var(--bg);
55 color: var(--text);
56 font: 400 0.92rem/1.7 var(--mono);
57 /* The 32px grid every site here draws, in two gradients and nothing else. */
58 background-image:
59 linear-gradient(rgba(107, 158, 120, 0.028) 1px, transparent 1px),
60 linear-gradient(90deg, rgba(107, 158, 120, 0.028) 1px, transparent 1px);
61 background-size: 32px 32px;
62 background-position: center top;
63 overflow: hidden;
64}
65
66/* The two overlays. Both are well below where search shipped them, because
67 measured with a multiply blend applied the body copy there came out at 3.9
68 to 1 against a nominal 15.5. */
69.scan, .vignette { position: fixed; inset: 0; pointer-events: none; }
70.scan {
71 z-index: 4;
72 background: repeating-linear-gradient(
73 to bottom, rgba(0,0,0,0.08) 0 1px, transparent 1px 3px);
74 mix-blend-mode: multiply;
75}
76.vignette {
77 z-index: 5;
78 background: radial-gradient(ellipse at center, transparent 62%, rgba(0,0,0,0.22) 100%);
79}
80
81/* ------------------------------------------------------------------ shell */
82
83/* --app-h is the visual viewport in pixels once the browser reports one, since
84 an open keyboard shrinks that and leaves every viewport unit unchanged. */
85.shell { display: flex; height: 100vh; height: var(--app-h, 100dvh); position: relative; z-index: 10; }
86
87/* ------------------------------------------------------------------ side */
88
89.side {
90 width: var(--side);
91 flex: 0 0 var(--side);
92 background: var(--void);
93 border-right: 1px solid var(--line);
94 display: flex;
95 flex-direction: column;
96 min-height: 0;
97 transition: margin-left 0.2s var(--ease);
98}
99.side[hidden], .side.closed { margin-left: calc(var(--side) * -1); }
100
101.side-head {
102 height: var(--barh);
103 flex: 0 0 var(--barh);
104 display: flex; align-items: center; gap: 0.5rem;
105 padding: 0 0.55rem 0 0.7rem;
106 border-bottom: 1px solid var(--line);
107}
108.brand { display: flex; align-items: center; gap: 0.5rem; text-decoration: none; margin-right: auto; }
109.key {
110 display: inline-flex; align-items: center; justify-content: center;
111 width: 1.35rem; height: 1.35rem;
112 border: 1px solid var(--line-hot);
113 color: var(--green-bright);
114 font-size: 0.72rem; font-weight: 700;
115 position: relative;
116}
117/* The cursor is drawn rather than typed, since a block glyph renders as a
118 hairline in some faces. */
119.cur {
120 position: absolute; right: 2px; bottom: 3px;
121 width: 3px; height: 7px; background: var(--green-hot);
122}
123@keyframes blink { 50% { opacity: 0; } }
124.brand-name { color: var(--text); font-weight: 700; letter-spacing: 0.02em; }
125
126.icon-btn {
127 background: none; border: 1px solid transparent; color: var(--dim);
128 font: inherit; font-size: 0.8rem; cursor: pointer; padding: 0.2rem 0.4rem;
129 border-radius: 2px; position: relative;
130}
131.icon-btn:hover { color: var(--green-hot); border-color: var(--line); background: var(--panel); }
132
133.new-chat {
134 margin: 0.65rem 0.7rem 0.2rem;
135 padding: 0.5rem 0.7rem;
136 background: var(--panel);
137 border: 1px solid rgba(92, 114, 92, 0.5);
138 color: var(--text);
139 font: inherit; text-align: left; cursor: pointer; border-radius: 2px;
140 display: flex; align-items: center; gap: 0.5rem;
141}
142.new-chat:hover { background: var(--panel-2); border-color: var(--green); color: var(--green-hot); }
143.plus { color: var(--green-bright); font-weight: 700; }
144
145.side-label {
146 padding: 0.9rem 0.85rem 0.35rem;
147 color: var(--amber);
148 font-size: 0.66rem; font-weight: 600; letter-spacing: var(--track); text-transform: uppercase;
149}
150
151.convs { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain;
152 padding: 0 0.45rem 0.5rem; min-height: 0; }
153.conv {
154 display: grid;
155 grid-template-columns: 1fr auto auto;
156 align-items: center; gap: 0.4rem;
157 padding: 0.4rem 0.5rem;
158 color: var(--muted); text-decoration: none;
159 border: 1px solid transparent; border-radius: 2px;
160 font-size: 0.8rem;
161}
162.conv:hover { background: var(--panel); border-color: rgba(65, 78, 65, 0.42); color: var(--text); }
163.conv.active { background: var(--panel); border-color: var(--line-hot); color: var(--green-hot); }
164
165/* A turn that finished while the reader was somewhere else. A dot rather than a
166 count, because the number of answers waiting is not a thing anybody acts on,
167 and the row already says which conversation it was. */
168.conv.done { position: relative; }
169.conv.done .conv-title::after {
170 content: "";
171 display: inline-block; width: 0.4rem; height: 0.4rem;
172 margin-left: 0.4rem; vertical-align: middle;
173 border-radius: 50%; background: var(--green-bright);
174}
175/* The same signal on the button that opens the drawer, since on a phone the
176 list it marks is off screen. */
177.icon-btn.dot-on::after {
178 content: "";
179 position: absolute; top: 0.3rem; right: 0.3rem;
180 width: 0.35rem; height: 0.35rem;
181 border-radius: 50%; background: var(--green-bright);
182}
183.conv-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
184.conv-when { color: var(--faint); font-size: 0.68rem; }
185.conv-del {
186 background: none; border: 0; color: var(--faint); cursor: pointer;
187 font-size: 0.66rem; padding: 0 0.15rem; opacity: 0; line-height: 1;
188}
189.conv:hover .conv-del { opacity: 1; }
190@media (hover: none) { .conv-del { opacity: 1; } }
191.conv-del:hover { color: var(--terracotta); }
192.empty { color: var(--faint); font-size: 0.78rem; padding: 0.3rem 0.5rem; margin: 0; }
193
194.side-foot { border-top: 1px solid var(--line); padding: 0 0.7rem 0.75rem; flex: 0 0 auto; }
195
196.incog { display: flex; gap: 0.55rem; align-items: flex-start; cursor: pointer; padding: 0.15rem 0; }
197.incog input { position: absolute; opacity: 0; width: 0; height: 0; }
198.incog-box {
199 width: 0.95rem; height: 0.95rem; margin-top: 0.28rem; flex: 0 0 auto;
200 border: 1px solid var(--line-hot); border-radius: 2px; position: relative;
201}
202.incog input:checked + .incog-box { border-color: var(--amber); }
203.incog input:checked + .incog-box::after {
204 content: ""; position: absolute; inset: 2px; background: var(--amber);
205}
206.incog input:focus-visible + .incog-box { outline: 2px solid var(--green-hot); outline-offset: 2px; }
207.incog-text { font-size: 0.76rem; line-height: 1.45; }
208.incog-text b { color: var(--text); font-weight: 600; }
209.incog-text small { display: block; color: var(--faint); font-size: 0.68rem; }
210
211.side-stats {
212 display: flex; gap: 0.8rem; padding: 0.6rem 0 0.5rem;
213 color: var(--faint); font-size: 0.68rem;
214}
215.side-stats b { color: var(--green-bright); font-weight: 600; }
216
217.wipe {
218 width: 100%; background: none; border: 1px solid rgba(196, 112, 85, 0.4);
219 color: var(--terracotta); font: inherit; font-size: 0.72rem;
220 padding: 0.3rem; cursor: pointer; border-radius: 2px;
221}
222.wipe:hover { background: rgba(196, 112, 85, 0.12); border-color: var(--terracotta); }
223
224/* ------------------------------------------------------------------ main */
225
226.main { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
227
228/* Chrome sits a step above the ground, never below it. A bar darker than the
229 page reads as a hole and leaves the small text on it nowhere to go. */
230.bar {
231 height: var(--barh); flex: 0 0 var(--barh);
232 display: flex; align-items: center; gap: 0.6rem;
233 padding: 0 0.9rem;
234 background: var(--bar);
235 border-bottom: 1px solid var(--line-hot);
236 position: relative; z-index: 20;
237}
238.bar-title {
239 color: var(--muted); font-size: 0.82rem;
240 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
241 margin-right: auto; min-width: 0;
242}
243.bar-right { display: flex; align-items: center; gap: 0.7rem; flex-shrink: 0; }
244.incog-flag {
245 color: var(--amber); font-size: 0.64rem; font-weight: 600;
246 letter-spacing: var(--track); border: 1px solid rgba(212, 180, 90, 0.5);
247 padding: 0.1rem 0.4rem; border-radius: 2px;
248}
249/* Amber is the warning here, per DESIGN.md, and this is the one tool failure
250 the bar reports: a turn without search answers from memory and otherwise
251 reads exactly like an answer that looked something up. */
252.warn-flag {
253 color: var(--terracotta); font-size: 0.64rem; font-weight: 600;
254 letter-spacing: var(--track); border: 1px solid rgba(196, 112, 85, 0.55);
255 padding: 0.1rem 0.4rem; border-radius: 2px; white-space: nowrap;
256}
257.warn-flag[hidden] { display: none; }
258
259.model { color: var(--dim); font-size: 0.72rem; display: flex; align-items: center; gap: 0.4rem; white-space: nowrap; }
260
261/* The context meter. A number beside a bar rather than a percentage, because
262 how many tokens are left is the thing being decided and a percentage of a
263 window whose size varies by model is not. */
264.meter { display: flex; align-items: center; gap: 0.4rem; font-size: 0.68rem; color: var(--faint); white-space: nowrap; }
265/* A display rule beats the hidden attribute, so anything here that sets
266 display has to say so itself or it shows through hidden. */
267.meter[hidden], .tps[hidden], .incog-flag[hidden] { display: none; }
268.meter-num { color: var(--dim); font-variant-numeric: tabular-nums; }
269.meter-cap { font-variant-numeric: tabular-nums; }
270.meter-track {
271 width: 4.5rem; height: 0.4rem;
272 border: 1px solid var(--line); border-radius: 1px;
273 background: var(--void); overflow: hidden;
274}
275.meter-fill {
276 display: block; height: 100%; width: 0;
277 background: var(--green); transition: width 0.3s var(--ease);
278}
279/* Over half the window is where compaction starts, so the bar says so before
280 it happens rather than after. */
281.meter.warm .meter-fill { background: var(--amber); }
282.meter.warm .meter-num { color: var(--amber); }
283.meter.hot .meter-fill { background: var(--terracotta); }
284.meter.hot .meter-num { color: var(--terracotta); }
285
286.tps { color: var(--faint); font-size: 0.68rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
287.tps b { color: var(--green-bright); font-weight: 600; }
288
289@media (max-width: 60em) {
290 .meter-track, .meter-cap { display: none; }
291}
292
293/* State is a ring that fills, not colour alone. */
294.dot {
295 width: 0.5rem; height: 0.5rem; border-radius: 50%;
296 border: 1px solid var(--line-hot); background: transparent;
297 display: inline-block; flex: 0 0 auto;
298}
299.dot.on { background: var(--green-bright); border-color: var(--green-bright); box-shadow: 0 0 6px var(--green-glow); }
300.dot.busy { background: var(--amber); border-color: var(--amber); animation: pulse 1s ease-in-out infinite; }
301@keyframes pulse { 50% { opacity: 0.35; } }
302
303.thread { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain;
304 padding: 1.6rem 1rem 1rem; min-height: 0; }
305
306/* ------------------------------------------------------------------ welcome */
307
308.welcome { max-width: var(--col); margin: 6vh auto 0; }
309.eyebrow {
310 display: inline-flex; align-items: center; gap: 0.45rem;
311 border: 1px solid var(--line); border-radius: 999px;
312 padding: 0.18rem 0.65rem; margin-bottom: 1.1rem;
313 color: var(--dim); font-size: 0.68rem; letter-spacing: 0.04em;
314}
315.eyebrow .sep { color: var(--faint); }
316.welcome h1 {
317 font-size: clamp(1.5rem, 3.4vw, 2.1rem); line-height: 1.3;
318 font-weight: 700; letter-spacing: -0.01em; margin: 0 0 0.8rem;
319}
320.welcome h1 .hl { color: var(--green-bright); }
321.welcome p { color: var(--muted); margin: 0 0 1.4rem; max-width: 42rem; }
322
323.welcome-hint { color: var(--faint); font-size: 0.72rem; margin: -0.6rem 0 1.1rem; }
324.welcome-hint kbd { margin-right: 0.15rem; }
325
326.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
327.chip {
328 background: var(--panel); border: 1px solid rgba(65, 78, 65, 0.42);
329 color: var(--muted); font: inherit; font-size: 0.78rem;
330 padding: 0.4rem 0.7rem; border-radius: 2px; cursor: pointer; text-align: left;
331}
332.chip:hover { background: var(--panel-2); border-color: var(--green); color: var(--green-hot); }
333
334/* ------------------------------------------------------------------ messages */
335
336.msg { max-width: var(--col); margin: 0 auto 1.6rem; }
337.who {
338 color: var(--amber); font-size: 0.64rem; font-weight: 600;
339 letter-spacing: var(--track); text-transform: uppercase; margin-bottom: 0.4rem;
340}
341.msg.user .who { color: var(--faint); }
342.msg.user .body {
343 background: var(--panel);
344 border: 1px solid rgba(65, 78, 65, 0.42);
345 border-left: 2px solid var(--line-hot);
346 padding: 0.7rem 0.9rem; border-radius: 2px;
347 color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere;
348}
349.msg.bot .body { color: var(--text); }
350.body > *:first-child { margin-top: 0; }
351.body > *:last-child { margin-bottom: 0; }
352.body p { margin: 0 0 0.85rem; }
353.body h1, .body h2, .body h3, .body h4 {
354 font-size: 1rem; font-weight: 700; letter-spacing: -0.01em;
355 margin: 1.4rem 0 0.5rem; color: var(--text);
356}
357.body h1 { font-size: 1.15rem; }
358.body strong { color: var(--green-bright); font-weight: 600; }
359.body a { color: var(--green-bright); text-decoration: underline; text-underline-offset: 2px; }
360.body a:hover { color: var(--green-hot); }
361.body ul, .body ol { margin: 0 0 0.9rem; padding-left: 1.2rem; }
362.body li { margin-bottom: 0.3rem; }
363.body li::marker { color: var(--green); }
364.body blockquote {
365 margin: 0 0 0.9rem; padding: 0.1rem 0 0.1rem 0.8rem;
366 border-left: 2px solid var(--line-hot); color: var(--muted);
367}
368.body code {
369 background: var(--panel); border: 1px solid rgba(65, 78, 65, 0.42);
370 padding: 0.05rem 0.3rem; border-radius: 2px; font-size: 0.86em; color: var(--green-hot);
371}
372.body pre {
373 background: var(--void); border: 1px solid var(--line);
374 border-left: 2px solid var(--green); border-radius: 2px;
375 padding: 0.8rem 0.9rem; overflow-x: auto; margin: 0 0 0.9rem;
376}
377.body pre code { background: none; border: 0; padding: 0; color: var(--text); font-size: 0.84rem; }
378.body table { border-collapse: collapse; width: 100%; margin: 0 0 0.9rem; font-size: 0.84rem; display: block; overflow-x: auto; }
379.body th, .body td { border: 1px solid rgba(65, 78, 65, 0.42); padding: 0.35rem 0.55rem; text-align: left; }
380.body th { color: var(--amber); font-weight: 600; letter-spacing: 0.03em; background: var(--panel); }
381.body hr { border: 0; border-top: 1px solid var(--line); margin: 1.2rem 0; }
382
383/* The unfinished paragraph. It is styled exactly like a rendered one so that
384 the moment it becomes a block nothing visibly moves. */
385.body .tail { margin: 0 0 0.85rem; white-space: pre-wrap; }
386.body .tail:empty { display: none; }
387
388/* The cursor while text is streaming in. */
389.body.typing .tail::after {
390 content: ""; display: inline-block; width: 0.45rem; height: 0.9rem;
391 background: var(--green-hot); margin-left: 2px; vertical-align: -0.12rem;
392 animation: blink 1.1s steps(1) infinite;
393}
394.body.typing .tail:empty { display: inline-block; min-height: 0.9rem; }
395
396/* ------------------------------------------------------------------ tools */
397
398.tools { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.7rem; }
399.tools[hidden] { display: none; }
400.tool {
401 display: inline-flex; align-items: center; gap: 0.35rem;
402 border: 1px solid rgba(65, 78, 65, 0.42); background: var(--panel);
403 color: var(--dim); font-size: 0.68rem; padding: 0.15rem 0.45rem; border-radius: 2px;
404}
405.tool b { color: var(--green-bright); font-weight: 600; }
406.tool .ms { color: var(--faint); }
407/* Amber per DESIGN.md, where it is the warning on a green site. The age of a
408 snapshot is the one thing on a chip a reader has to weigh. */
409.tool .age { color: var(--amber); }
410.tool.bad { border-color: rgba(196, 112, 85, 0.5); }
411.tool.bad b { color: var(--terracotta); }
412.tool.run b { color: var(--amber); }
413
414/* The number after a sentence, linking to the page it came from. Raised rather
415 than superscripted, since a real <sup> at this size loses a pixel of the
416 digit and the tap target with it. */
417.cite {
418 display: inline-block; min-width: 1.05rem; text-align: center;
419 margin-left: 0.18rem; padding: 0 0.18rem;
420 border: 1px solid rgba(65, 78, 65, 0.55); border-radius: 2px;
421 background: var(--panel); color: var(--green-bright);
422 font-size: 0.62rem; line-height: 1.25; vertical-align: 0.32em;
423 text-decoration: none; font-variant-numeric: tabular-nums;
424}
425.cite:hover, .cite:focus-visible { border-color: var(--green-bright); background: var(--green-glow); color: var(--green-hot); }
426
427.sources { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.7rem; }
428.sources[hidden] { display: none; }
429.src {
430 display: inline-flex; align-items: center; gap: 0.35rem;
431 max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
432 border: 1px solid rgba(65, 78, 65, 0.42); background: var(--panel);
433 color: var(--dim); font-size: 0.68rem; padding: 0.15rem 0.45rem; border-radius: 2px;
434 text-decoration: none;
435}
436.src b { color: var(--green-bright); font-weight: 600; }
437.src:hover, .src:focus-visible { border-color: var(--green-bright); color: var(--text); }
438
439/* Room under the newest exchange so its question can sit at the top of the
440 viewport. It shrinks to nothing once the answer is long enough to fill the
441 screen by itself. */
442.spacer { height: 0; flex: 0 0 auto; }
443
444.status {
445 max-width: var(--col); margin: 0 auto 1.2rem;
446 color: var(--dim); font-size: 0.76rem;
447 display: flex; align-items: center; gap: 0.5rem;
448}
449.status .dots::after {
450 content: ""; animation: dots 1.4s steps(4, end) infinite;
451}
452@keyframes dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }
453
454.err {
455 max-width: var(--col); margin: 0 auto 1.4rem;
456 border: 1px solid rgba(196, 112, 85, 0.5); border-left: 2px solid var(--terracotta);
457 background: rgba(196, 112, 85, 0.08); color: var(--muted);
458 padding: 0.6rem 0.8rem; border-radius: 2px; font-size: 0.82rem;
459}
460
461/* ------------------------------------------------------------------ composer */
462
463.composer {
464 flex: 0 0 auto; padding: 0.5rem 1rem 0.7rem;
465 border-top: 1px solid var(--line);
466 background: linear-gradient(to top, var(--bg) 70%, transparent);
467}
468.ask {
469 max-width: var(--col); margin: 0 auto;
470 display: flex; align-items: flex-end; gap: 0.5rem;
471 background: var(--panel);
472 border: 1px solid rgba(92, 114, 92, 0.5);
473 border-radius: 3px; padding: 0.4rem 0.4rem 0.4rem 0.7rem;
474}
475.ask:focus-within { border-color: var(--green); box-shadow: 0 0 0 3px var(--green-glow); }
476.ask textarea {
477 flex: 1 1 auto; background: none; border: 0; resize: none;
478 color: var(--text); font: inherit; line-height: 1.6;
479 max-height: 11rem; padding: 0.25rem 0; outline: none;
480}
481.ask textarea::placeholder { color: var(--faint); }
482.ask button {
483 flex: 0 0 auto; width: 2rem; height: 2rem;
484 background: var(--green); border: 0; border-radius: 2px;
485 color: var(--void); font-size: 0.72rem; cursor: pointer;
486}
487.ask button:hover { background: var(--green-hot); }
488.ask button:disabled { background: var(--panel-2); color: var(--faint); cursor: not-allowed; }
489#stop { background: var(--terracotta); color: var(--void); }
490
491/* ------------------------------------------------------------- attachments */
492
493/* The attach button is the one control in the composer that is not the send
494 action, so it reads as chrome rather than taking the green. */
495.ask .attach {
496 background: none; border: 1px solid rgba(92, 114, 92, 0.5);
497 color: var(--dim); font-size: 1rem; line-height: 1;
498}
499.ask .attach:hover { background: var(--panel-2); color: var(--green-bright); border-color: var(--line-hot); }
500
501.tray {
502 max-width: var(--col); margin: 0 auto 0.4rem;
503 display: flex; flex-wrap: wrap; gap: 0.35rem;
504}
505.tray[hidden] { display: none; }
506.chip {
507 display: inline-flex; align-items: center; gap: 0.4rem;
508 border: 1px solid rgba(92, 114, 92, 0.5); background: var(--panel);
509 color: var(--muted); font-size: 0.7rem; padding: 0.2rem 0.25rem 0.2rem 0.5rem;
510 border-radius: 2px; max-width: 18rem;
511}
512.chip .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
513.chip .sz { color: var(--faint); }
514.chip.bad { border-color: rgba(196, 112, 85, 0.5); color: var(--terracotta); }
515.chip button {
516 background: none; border: 0; color: var(--faint); cursor: pointer;
517 font-size: 0.7rem; line-height: 1; padding: 0.15rem 0.2rem;
518}
519.chip button:hover { color: var(--terracotta); }
520
521/* Attachment chips on a sent message. Same shape as the tool chips above them
522 and not interactive, since the file itself was never kept. */
523.files { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.7rem; }
524.files[hidden] { display: none; }
525
526/* The drop target is the whole window, because a file dragged out of a file
527 manager is released wherever the pointer is rather than over the composer. */
528.drop {
529 position: fixed; inset: 0; z-index: 40;
530 display: flex; align-items: center; justify-content: center;
531 background: rgba(10, 9, 8, 0.82); backdrop-filter: blur(2px);
532 border: 2px dashed var(--green); pointer-events: none;
533}
534.drop[hidden] { display: none; }
535.drop span {
536 color: var(--green-hot); font-size: 0.8rem; font-weight: 700;
537 letter-spacing: var(--track); text-transform: uppercase;
538}
539
540.foot-note {
541 max-width: var(--col); margin: 0.4rem auto 0;
542 display: flex; justify-content: space-between; gap: 1rem;
543 color: var(--faint); font-size: 0.68rem;
544}
545.foot-note a { color: var(--faint); text-decoration: none; }
546.foot-note a:hover { color: var(--green-bright); text-decoration: underline; }
547
548/* ------------------------------------------------------------------ keys */
549
550.filter {
551 margin: 0 0.45rem 0.4rem; width: calc(100% - 0.9rem);
552 background: var(--panel); border: 1px solid rgba(92, 114, 92, 0.5);
553 color: var(--text); font: inherit; font-size: 0.78rem;
554 padding: 0.25rem 0.45rem; border-radius: 2px;
555}
556.filter[hidden] { display: none; }
557.filter:focus { outline: none; border-color: var(--green); }
558
559.sheet {
560 position: fixed; inset: 0; z-index: 60;
561 background: rgba(10, 9, 8, 0.72);
562 display: flex; align-items: center; justify-content: center; padding: 1rem;
563}
564.sheet[hidden] { display: none; }
565.sheet-card {
566 background: var(--panel); border: 1px solid var(--line-hot); border-radius: 3px;
567 padding: 1rem 1.1rem; width: min(30rem, 100%);
568 box-shadow: 0 20px 60px rgba(0,0,0,0.6);
569}
570.sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; }
571
572.keys { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 0.9rem; margin: 0; align-items: center; }
573.keys dt { display: flex; gap: 0.2rem; }
574.keys dd { margin: 0; color: var(--muted); font-size: 0.8rem; }
575kbd {
576 background: var(--void); border: 1px solid var(--line-hot);
577 border-bottom-width: 2px; border-radius: 2px;
578 padding: 0.05rem 0.32rem; font: inherit; font-size: 0.7rem; color: var(--green-bright);
579}
580.keys-note { color: var(--faint); font-size: 0.7rem; margin: 0.9rem 0 0; line-height: 1.6; }
581.keys-note kbd { font-size: 0.66rem; }
582
583.foot-right { display: flex; gap: 0.8rem; align-items: center; }
584.linkish {
585 background: none; border: 0; color: var(--faint); font: inherit; font-size: 0.68rem;
586 cursor: pointer; padding: 0; text-decoration: none;
587}
588.linkish:hover { color: var(--green-bright); text-decoration: underline; }
589.keys-hint { display: inline-flex; align-items: center; gap: 0.2rem; }
590.keys-hint kbd { font-size: 0.62rem; padding: 0 0.25rem; border-bottom-width: 1px; }
591.keys-hint:hover { text-decoration: none; }
592.keys-hint:hover kbd { border-color: var(--green); color: var(--green-hot); }
593
594
595
596@media (prefers-reduced-motion: reduce) {
597 * { animation: none !important; transition: none !important; }
598}
599
600/* ---------------------------------------------------------------- landing */
601
602/* The one page a signed out visitor sees. It reuses the palette and the brand
603 mark and nothing else, since the app's layout is a three pane shell and this
604 is a single column of prose. */
605
606.landing-body { display: block; overflow-y: auto; }
607
608.landing {
609 max-width: 42rem;
610 margin: 0 auto;
611 padding: 4rem 1.4rem 5rem;
612}
613.landing .brand { display: inline-flex; align-items: center; gap: 0.5rem; text-decoration: none; }
614.landing .brand:hover { text-decoration: none; }
615
616.landing .eyebrow {
617 color: var(--amber); font-size: 0.64rem; font-weight: 600;
618 letter-spacing: var(--track); text-transform: uppercase;
619 margin: 1.6rem 0 0.6rem;
620}
621.landing h1 {
622 font-size: 1.7rem; line-height: 1.3; font-weight: 700;
623 letter-spacing: -0.01em; margin: 0 0 1rem; color: var(--text);
624}
625.landing h1 .accent { color: var(--green-bright); }
626.landing .lede { color: var(--muted); font-size: 0.92rem; margin: 0 0 1.6rem; }
627
628.landing .points { list-style: none; padding: 0; margin: 0 0 2rem; }
629.landing .points li {
630 color: var(--dim); font-size: 0.82rem;
631 padding: 0.5rem 0 0.5rem 1rem;
632 border-left: 1px solid var(--line);
633 margin-bottom: 0.3rem;
634}
635.landing .points b { color: var(--green-bright); font-weight: 700; }
636
637.landing .ctas { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 2.4rem; }
638.landing .btn {
639 display: inline-block; background: var(--green); color: var(--void);
640 border: 1px solid var(--green); border-radius: 2px;
641 font-size: 0.78rem; font-weight: 700; padding: 0.45rem 0.9rem;
642 text-decoration: none;
643}
644.landing .btn:hover { background: var(--green-hot); border-color: var(--green-hot); text-decoration: none; }
645.landing .btn.ghost { background: none; color: var(--dim); border-color: var(--line); }
646.landing .btn.ghost:hover { color: var(--green-bright); border-color: var(--line-hot); }
647
648
649/* ------------------------------------------------------- memory overlay */
650
651/* Taller than the shortcuts sheet because the list grows, and scrolling
652 inside the card rather than the page so the conversation behind it does
653 not move while you read. */
654.mem-card { width: min(38rem, 100%); max-height: min(80vh, 42rem); display: flex; flex-direction: column; }
655.mem-lede { color: var(--dim); font-size: 0.74rem; margin: 0 0 0.7rem; }
656
657.mem-msg {
658 border-left: 2px solid var(--green); background: var(--void);
659 padding: 0.4rem 0.6rem; margin-bottom: 0.7rem;
660 font-size: 0.76rem; color: var(--muted);
661}
662.mem-msg[hidden] { display: none; }
663.mem-msg.bad { border-left-color: var(--terracotta); color: var(--terracotta); }
664
665.teach { display: flex; gap: 0.5rem; align-items: flex-end; margin-bottom: 0.8rem; }
666.teach textarea {
667 flex: 1 1 auto; background: var(--void); color: var(--text);
668 border: 1px solid rgba(92, 114, 92, 0.5); border-radius: 3px;
669 font: inherit; font-size: 0.82rem; line-height: 1.5;
670 padding: 0.4rem 0.55rem; resize: vertical; max-height: 8rem;
671}
672.teach textarea:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px var(--green-glow); }
673.teach textarea::placeholder { color: var(--faint); }
674.teach button {
675 flex: 0 0 auto; background: var(--green); color: var(--void); border: 0;
676 border-radius: 2px; font: inherit; font-size: 0.74rem; font-weight: 700;
677 padding: 0.45rem 0.75rem; cursor: pointer;
678}
679.teach button:hover { background: var(--green-hot); }
680.teach button:disabled { background: var(--panel-2); color: var(--faint); cursor: not-allowed; }
681
682.mem-list { flex: 1 1 auto; overflow-y: auto; min-height: 3rem; }
683.fact {
684 display: flex; align-items: baseline; gap: 0.6rem;
685 padding: 0.45rem 0; border-bottom: 1px solid rgba(65, 78, 65, 0.45);
686}
687.fact:last-child { border-bottom: 0; }
688.fact .ft { flex: 1 1 auto; color: var(--text); font-size: 0.82rem; }
689.fact .fu { flex: 0 0 auto; color: var(--faint); font-size: 0.64rem; white-space: nowrap; }
690.fact .fx {
691 flex: 0 0 auto; background: none; border: 0; color: var(--faint);
692 cursor: pointer; font-size: 0.68rem; padding: 0.1rem 0.2rem;
693}
694.fact .fx:hover { color: var(--terracotta); }
695.fact.going { opacity: 0.35; }
696
697.mem-empty { color: var(--faint); font-size: 0.78rem; padding: 0.6rem 0; }
698
699.mem-foot {
700 display: flex; align-items: center; justify-content: space-between;
701 gap: 1rem; margin-top: 0.8rem; padding-top: 0.6rem;
702 border-top: 1px solid var(--line);
703 color: var(--faint); font-size: 0.68rem;
704}
705.mem-foot .danger {
706 background: none; border: 1px solid rgba(196, 112, 85, 0.5);
707 color: var(--terracotta); border-radius: 2px;
708 font: inherit; font-size: 0.68rem; padding: 0.25rem 0.5rem; cursor: pointer;
709}
710.mem-foot .danger:hover { background: rgba(196, 112, 85, 0.12); }
711
712/* ------------------------------------------------------------------ narrow */
713
714@media (max-width: 52em) {
715 /* Wider than the desktop rail, because a conversation title has to be
716 readable and 264px of a phone truncates most of them. It stays short of
717 the full width so the dimmed strip says there is a page behind it. */
718 .side {
719 --side: min(85vw, 20rem);
720 position: fixed; inset: 0 auto 0 0; z-index: 40;
721 width: var(--side); flex-basis: var(--side);
722 box-shadow: 0 0 40px rgba(0,0,0,0.6);
723 margin-left: 0;
724 transition: transform 0.2s var(--ease);
725 }
726 /* Off screen by transform rather than by margin, since a margin animation
727 relays out the page every frame and scores as a layout shift. */
728 .side.closed { margin-left: 0; transform: translateX(-100%); }
729
730 /* Two rows, because a title and a timestamp side by side on a phone leaves
731 the title about twelve characters. It also makes the row a thumb high. */
732 .conv {
733 grid-template-columns: 1fr auto;
734 grid-template-areas: "title del" "when del";
735 row-gap: 0.05rem;
736 padding: 0.5rem;
737 }
738 .conv-title { grid-area: title; }
739 .conv-when { grid-area: when; }
740 .conv-del { grid-area: del; align-self: center; }
741 .thread { padding: 1.2rem 0.8rem 0.8rem; }
742 .welcome { margin-top: 3vh; }
743
744 /* Everything about a keyboard, on a device without one. */
745 .welcome-hint, #foot-state, .keys-hint { display: none; }
746
747 /* What is left is two links, and they fit on one line once the keyboard
748 advice above is gone. */
749 .foot-note { justify-content: flex-end; font-size: 0.72rem; }
750 .foot-right { gap: 0.9rem; }
751 .foot-note a, .foot-note .linkish { white-space: nowrap; }
752
753 /* iOS zooms the whole page when a field it focuses is under 16px, and then
754 leaves it zoomed, which is the single worst thing that happens on a phone
755 here. The value is in px because it is about the browser's rule and not
756 about this page's type scale. */
757 .ask textarea, .teach textarea, .filter { font-size: 16px; }
758
759 /* Touch targets. 32px is comfortable with a mouse and small with a thumb. */
760 .ask button { width: 2.75rem; height: 2.75rem; font-size: 0.9rem; }
761 .icon-btn { min-width: 2.5rem; min-height: 2.5rem; padding: 0.4rem 0.5rem; }
762 .chip { padding: 0.5rem 0.7rem; }
763 .fact .fx { padding: 0.4rem 0.5rem; }
764 .conv-del { padding: 0.4rem 0.5rem; }
765 .tray .chip button { padding: 0.35rem 0.45rem; }
766 .incog { padding: 0.4rem 0; }
767
768 /* The bar is the tightest row on the page, so it gives up its padding first
769 and the model name stops wrapping to a second line. */
770 .bar { gap: 0.45rem; padding: 0 0.55rem; }
771 .model { font-size: 0.66rem; white-space: nowrap; }
772 .bar-title { font-size: 0.76rem; }
773
774 /* Dimming what is behind it is what says the sidebar is a layer rather than
775 part of the page, and tapping it is how a phone expects to close one. It
776 has to be inside .shell, which carries a z-index of its own and so makes a
777 stacking context the sidebar cannot climb out of. */
778 .scrim {
779 position: fixed; inset: 0; z-index: 35;
780 background: rgba(10, 9, 8, 0.6);
781 }
782 .scrim[hidden] { display: none; }
783
784 /* A tap is a click here and never a double tap to zoom, so the browser has
785 no reason to hold the gesture for 300ms first. */
786 button, .conv, .chip, a { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
787
788 .sheet { padding: 0.5rem; align-items: flex-start; padding-top: 3vh; }
789 .sheet-card { padding: 0.8rem 0.85rem; }
790 .mem-card { max-height: calc(var(--app-h, 100dvh) - 2.5rem); }
791 .teach { flex-direction: column; align-items: stretch; }
792 .teach button { width: 100%; padding: 0.6rem; }
793}
794
795/* The sidebar is a layer at every width where it overlays, and the scrim only
796 ever exists there. */
797.scrim { display: none; }
798@media (max-width: 52em) {
799 .scrim:not([hidden]) { display: block; }
800}
801
802/* Widgets ------------------------------------------------------------------
803
804 The panel a ticker or a forecast draws above the answer. The language is
805 dash's, since that is where these readings already live here and a second
806 visual grammar for the same numbers would be worse than a familiar one, but
807 the palette is this site's green rather than dash's amber. Direction is the
808 one place a colour means something, so up and down are defined here and used
809 nowhere else. */
810
811:root {
812 --up: #7fc98f;
813 --up-soft: rgba(127, 201, 143, 0.09);
814 --down: #d98063;
815 --down-soft: rgba(217, 128, 99, 0.09);
816 --flat: #9c8e7a;
817}
818
819/* The hidden attribute is a UA rule of one specificity, so any display set
820 below beats it, and on an svg child it is not honoured at all. Both the
821 readout and the cursor lines are toggled with it, so it is restated here. */
822.wdg [hidden] { display: none !important; }
823
824.widgets {
825 display: flex;
826 flex-direction: column;
827 gap: 0.7rem;
828 margin: 0 0 0.9rem;
829}
830
831.wdg {
832 border: 1px solid var(--line);
833 background: var(--panel);
834 font-size: 0.82rem;
835}
836
837.wdg-head {
838 display: flex;
839 align-items: flex-start;
840 justify-content: space-between;
841 gap: 1rem;
842 padding: 0.6rem 0.75rem 0.5rem;
843}
844
845.wdg-id { min-width: 0; }
846
847.wdg-sym {
848 display: block;
849 font-size: 0.9rem;
850 letter-spacing: 0.06em;
851 color: var(--text);
852}
853
854.wdg-name {
855 display: block;
856 font-size: 0.72rem;
857 color: var(--dim);
858 white-space: nowrap;
859 overflow: hidden;
860 text-overflow: ellipsis;
861 max-width: 22rem;
862}
863
864.wdg-quote { text-align: right; white-space: nowrap; }
865
866.wdg-price {
867 display: block;
868 font-size: 1.25rem;
869 letter-spacing: -0.02em;
870 color: var(--text);
871}
872
873.wdg-move { display: block; font-size: 0.75rem; }
874.wdg-chg { color: var(--dim); }
875.wdg-pct { margin-left: 0.45rem; color: var(--dim); }
876
877.wdg[data-dir="up"] .wdg-pct, .wdg[data-dir="up"] .wdg-chg { color: var(--up); }
878.wdg[data-dir="down"] .wdg-pct, .wdg[data-dir="down"] .wdg-chg { color: var(--down); }
879.wdg[data-dir="flat"] .wdg-pct, .wdg[data-dir="flat"] .wdg-chg { color: var(--flat); }
880
881.wdg-ranges {
882 display: flex;
883 gap: 1px;
884 padding: 0 0.75rem 0.55rem;
885}
886
887.wdg-ranges button {
888 font: inherit;
889 font-size: 0.7rem;
890 letter-spacing: var(--track);
891 padding: 0.2rem 0.55rem;
892 color: var(--faint);
893 background: transparent;
894 border: 1px solid transparent;
895 cursor: pointer;
896}
897
898.wdg-ranges button:hover { color: var(--text); }
899
900.wdg-ranges button.on {
901 color: var(--green-hot);
902 border-color: var(--line-hot);
903 background: var(--panel-2);
904}
905
906/* The plot is the positioning context for the cursor dot and the readout,
907 which are HTML over the top of the svg. The box is stretched to the column
908 width, so a circle drawn inside it would come out an ellipse. */
909.wdg-plot { position: relative; }
910
911.wdg-plot svg {
912 display: block;
913 width: 100%;
914 height: 8.5rem;
915 touch-action: pan-y;
916 cursor: crosshair;
917}
918
919.wdg-line {
920 fill: none;
921 stroke: var(--flat);
922 stroke-width: 1.4;
923 stroke-linejoin: round;
924 stroke-linecap: round;
925 vector-effect: non-scaling-stroke;
926}
927
928.wdg-area { stroke: none; fill: rgba(156, 142, 122, 0.07); }
929
930.wdg[data-dir="up"] .wdg-line { stroke: var(--up); }
931.wdg[data-dir="up"] .wdg-area { fill: var(--up-soft); }
932.wdg[data-dir="down"] .wdg-line { stroke: var(--down); }
933.wdg[data-dir="down"] .wdg-area { fill: var(--down-soft); }
934
935/* Yesterday's close, which is what the intraday chart is measured against and
936 what makes its shape mean anything on its own. */
937.wdg-base {
938 stroke: var(--faint);
939 stroke-width: 1;
940 stroke-dasharray: 1 4;
941 vector-effect: non-scaling-stroke;
942}
943
944.wdg-cursor, .wdg-anchor {
945 stroke: var(--green-hot);
946 stroke-width: 1;
947 vector-effect: non-scaling-stroke;
948}
949
950.wdg-anchor { stroke-dasharray: 2 3; opacity: 0.7; }
951.wdg-band { fill: var(--green-glow); }
952
953.wdg-dot {
954 position: absolute;
955 width: 7px;
956 height: 7px;
957 margin: -3.5px 0 0 -3.5px;
958 border-radius: 50%;
959 background: var(--text);
960 pointer-events: none;
961}
962
963.wdg-dot-a { background: var(--faint); }
964
965.wdg-read {
966 position: absolute;
967 top: 0.3rem;
968 display: flex;
969 gap: 0.5rem;
970 align-items: baseline;
971 padding: 0.25rem 0.5rem;
972 background: var(--bar);
973 border: 1px solid var(--line-hot);
974 font-size: 0.72rem;
975 white-space: nowrap;
976 pointer-events: none;
977}
978
979.wdg-read[data-side="right"] { transform: translateX(0.5rem); }
980.wdg-read[data-side="left"] { transform: translateX(calc(-100% - 0.5rem)); }
981.wdg-read-pct[data-dir="up"] { color: var(--up); }
982.wdg-read-pct[data-dir="down"] { color: var(--down); }
983.wdg-read-pct[data-dir="flat"] { color: var(--flat); }
984.wdg-read-at { color: var(--faint); }
985
986.wdg-msg {
987 margin: 0;
988 padding: 1.6rem 0.75rem;
989 text-align: center;
990 color: var(--faint);
991 font-size: 0.75rem;
992}
993
994.wdg-plot .wdg-msg:empty { display: none; }
995.wdg.loading { opacity: 0.65; }
996
997.wdg-foot {
998 display: flex;
999 justify-content: space-between;
1000 gap: 1rem;
1001 padding: 0.4rem 0.75rem 0.5rem;
1002 border-top: 1px solid var(--line);
1003 font-size: 0.68rem;
1004 letter-spacing: 0.04em;
1005 color: var(--faint);
1006}
1007
1008/* Weather ------------------------------------------------------------------ */
1009
1010.wdg-tiles {
1011 display: grid;
1012 grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
1013 gap: 1px;
1014 background: var(--line);
1015 border-top: 1px solid var(--line);
1016 border-bottom: 1px solid var(--line);
1017}
1018
1019.wdg-tile {
1020 background: var(--panel-2);
1021 padding: 0.45rem 0.6rem 0.5rem;
1022}
1023
1024.wdg-tile-k {
1025 display: block;
1026 font-size: 0.62rem;
1027 letter-spacing: var(--track);
1028 text-transform: uppercase;
1029 color: var(--faint);
1030}
1031
1032.wdg-tile-v { display: block; font-size: 1rem; color: var(--text); }
1033.wdg-tile-n { display: block; font-size: 0.66rem; color: var(--dim); }
1034.wdg-tile[data-dir="up"] .wdg-tile-v { color: var(--up); }
1035.wdg-tile[data-dir="down"] .wdg-tile-v { color: var(--down); }
1036
1037.wdg-days { list-style: none; margin: 0; padding: 0; }
1038
1039.wdg-day {
1040 display: grid;
1041 grid-template-columns: 4.2rem 3.2rem 6rem 1fr;
1042 align-items: baseline;
1043 gap: 0.5rem;
1044 padding: 0.32rem 0.75rem;
1045 border-top: 1px solid var(--line);
1046}
1047
1048.wdg-day:first-child { border-top: none; }
1049.wdg-day-k { color: var(--muted); font-size: 0.75rem; }
1050.wdg-day-rain { font-size: 0.72rem; color: var(--faint); text-align: right; }
1051.wdg-day-rain[data-wet="yes"] { color: var(--green-hot); }
1052.wdg-day-t b { color: var(--text); }
1053.wdg-day-t i { color: var(--faint); font-style: normal; margin-left: 0.5rem; }
1054
1055.wdg-day-feels {
1056 font-size: 0.72rem;
1057 color: var(--faint);
1058}
1059
1060@media (max-width: 34rem) {
1061 .wdg-name { max-width: 11rem; }
1062 .wdg-day { grid-template-columns: 3.4rem 2.8rem 1fr; }
1063 .wdg-day-feels { display: none; }
1064}
1065
1066/* The record of what a turn did. Open while it runs and collapsed after, since
1067 the answer is what the reader came for and the work is what they check. The
1068 caps label and the amber are DESIGN.md's, and the kind of a step is the only
1069 thing here that carries colour, so the list reads as one object. */
1070.work { margin: 0.15rem 0 0.7rem; }
1071.work[hidden] { display: none; }
1072.work-head {
1073 display: flex; align-items: center; gap: 0.5rem; width: 100%;
1074 background: none; border: 0; padding: 0.2rem 0; cursor: pointer;
1075 color: var(--dim); font: inherit; text-align: left;
1076}
1077.work-k {
1078 color: var(--amber); font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase;
1079}
1080.work-sum { color: var(--faint); font-size: 0.75rem; }
1081.work-head .chev {
1082 width: 0; height: 0; flex: none;
1083 border-left: 4px solid var(--faint);
1084 border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
1085 transition: transform 120ms ease;
1086}
1087.work-head[aria-expanded="true"] .chev { transform: rotate(90deg); }
1088.work-head:hover .work-sum, .work-head:hover .chev { color: var(--green-bright); }
1089
1090.work-steps {
1091 list-style: none; margin: 0.3rem 0 0; padding: 0 0 0 0.42rem;
1092 border-left: 1px solid rgba(65, 78, 65, 0.42);
1093}
1094.work-steps[hidden] { display: none; }
1095
1096.step { position: relative; }
1097.step-head {
1098 display: flex; align-items: baseline; gap: 0.45rem; width: 100%;
1099 background: none; border: 0; padding: 0.24rem 0; cursor: pointer;
1100 color: var(--dim); font: inherit; font-size: 0.8rem; text-align: left;
1101}
1102.step-head[disabled] { cursor: default; }
1103/* The dot sits on the rule the list hangs from, so the steps read as one run
1104 down the page rather than as separate rows. */
1105.step .dot {
1106 flex: none; width: 5px; height: 5px; border-radius: 50%;
1107 margin-left: -0.42rem; transform: translateX(-50%);
1108 background: var(--green-bright);
1109}
1110.step[data-kind="wikipedia"] .dot { background: var(--amber); }
1111.step[data-kind="gate"] .dot { background: var(--amber); }
1112.step.bad .dot { background: var(--terracotta); }
1113.step .k {
1114 flex: none; color: var(--green-bright);
1115 letter-spacing: 0.12em; text-transform: uppercase; font-size: 0.7rem;
1116}
1117.step[data-kind="wikipedia"] .k, .step[data-kind="gate"] .k { color: var(--amber); }
1118.step.bad .k { color: var(--terracotta); }
1119/* Both the label and the detail give way on a narrow screen rather than
1120 pushing the time off the edge, and the detail goes first because the label
1121 is what names the step. */
1122.step .l {
1123 flex: 0 1 auto; min-width: 0; color: var(--dim);
1124 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1125}
1126/* The glance at what a step actually did. It takes whatever width is left and
1127 is the only part of the row that truncates, so the kind, the label and the
1128 time never move as one arrives. */
1129.step .d {
1130 /* Capped so a long url can never squeeze the tool name beside it, which is
1131 the one word on the row worth reading. */
1132 flex: 1 1 auto; min-width: 0; max-width: 52%;
1133 color: var(--faint); opacity: 0.72; margin-left: 0.15rem;
1134 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1135}
1136.step-head:hover .d { color: var(--dim); opacity: 1; }
1137
1138/* A phone has room for the kind, the label and the time and not for the
1139 argument as well, and a row truncated to "web_sea…" has lost the one word
1140 that made it worth reading. The glance is a wide screen affordance, and the
1141 step still opens on a tap. */
1142@media (max-width: 34rem) {
1143 .step .d { display: none; }
1144}
1145/* Pushed right by the margin rather than by whatever is beside it, so the
1146 times line up as a column whether or not a row carries a detail. */
1147.step .ms { flex: none; margin-left: auto; padding-left: 0.5rem; color: var(--faint); }
1148.step-head:hover .l { color: var(--text); }
1149
1150.step-body { margin: 0.1rem 0 0.45rem 0.55rem; }
1151.step-body[hidden] { display: none; }
1152.step-meta { color: var(--faint); font-size: 0.72rem; margin-bottom: 0.3rem; }
1153.io { display: flex; gap: 0.45rem; margin-bottom: 0.3rem; }
1154.io-k {
1155 flex: none; width: 2rem; color: var(--amber);
1156 font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; padding-top: 0.15rem;
1157}
1158.io pre {
1159 flex: 1 1 auto; margin: 0; max-height: 13rem; overflow: auto;
1160 border: 1px solid rgba(65, 78, 65, 0.42); background: var(--panel);
1161 padding: 0.35rem 0.45rem; border-radius: 2px;
1162 color: var(--dim); font-size: 0.75rem; line-height: 1.6;
1163 white-space: pre-wrap; overflow-wrap: anywhere;
1164}