Repository navigation
Expand file tree
/
Copy pathindex.css
More file actions
1250 lines (1149 loc) · 58.4 KB
/
Copy pathindex.css
File metadata and controls
1250 lines (1149 loc) · 58.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Every `@import` first: CSS requires them before any other rule, and an
`@import` sitting after `@plugin` is ignored by a browser reading this file
directly (Tailwind resolves them at build time, which is what hid it). */
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist";
@import "@fontsource-variable/geist-mono";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
/* ============================================================================
* OpenCompany design tokens
*
* Three layers, in this order, and nothing skips a layer:
*
* 1. PRIMITIVES — raw ramps (`--brand-500`, `--surface-dark-2`,
* `--ink-light-hint`, `--green-mark`).
* Theme-independent. Never referenced by a component.
* 2. SEMANTICS — what a colour *means* here (`--primary`, `--status-running`,
* `--border`). Light lives in `:root`, dark overrides in
* `.dark`. This is the layer components are written against.
* 3. UTILITIES — the `@theme` blocks that turn semantics into Tailwind
* classes (`bg-primary`, `text-status-running`).
*
* The rule that keeps this honest: a component may only use layer 3. If a
* component needs a colour that layer 2 does not name, the fix is to name it
* here — not to reach past it into a ramp or an arbitrary value.
*
* See `docs/design-system/README.md` for the full reference and
* `docs/brand/README.md` for why these values and not others.
* ========================================================================== */
:root {
/* ------------------------------------------------------------------
* LAYER 1 — PRIMITIVES
*
* Stated in oklch, converted from the hex values the brand doc names.
* oklch because the ramps were tuned for even *perceptual* steps: an
* equal jump in L is an equal jump in apparent lightness, which sRGB
* hex does not give you. The hex on each line is the canonical value —
* that is what goes into Figma, a slide, or a favicon.
* ---------------------------------------------------------------- */
/* Brand — violet. The one hue the product owns.
Reserved for interaction and identity: anything the operator can
click, the active surface they are on, and the marks that stand for
the company itself. It is never a status.
500 is the value the brand guide names; every other step holds this
ramp's original lightness cadence and hue drift, with chroma scaled so
500 lands exactly on it. Every step verified in sRGB gamut. */
--brand-50: oklch(0.9522 0.0230 295.71); /* #F0ECFD */
--brand-100: oklch(0.9124 0.0427 295.48); /* #E4DDFC */
--brand-200: oklch(0.841 0.0794 294.74); /* #CEC1FA */
--brand-300: oklch(0.7488 0.1287 292.75); /* #B09DF8 */
--brand-400: oklch(0.6614 0.1769 289.73); /* #937BF6 */
--brand-500: oklch(0.5649 0.2236 285.51); /* #7153F0 — the brand */
--brand-600: oklch(0.5141 0.2088 284.97); /* #6247D7 */
--brand-700: oklch(0.4453 0.1828 284.88); /* #5038B2 */
--brand-800: oklch(0.369 0.1496 285.32); /* #3C2A89 */
--brand-900: oklch(0.2957 0.1142 286.51); /* #2B1E62 */
/*
* Surfaces — the six-rung ladder, stated per theme.
*
* One ramp cannot hold both themes here. The brand guide draws light and
* dark as two independent ladders whose rungs carry the same *roles*, not
* the same lightnesses, and the roles are what the semantic layer binds
* to. So the rung is the primitive, and the theme picks a set.
*
* Rung meanings, fixed by the guide:
* bg main content canvas
* 1 sidebar · cards · panels
* 2 panel stroke · date badge
* 3 icon circles
* 4 card stroke · dividers
* active the nav row you are standing on
*
* Every value below is the guide's, unchanged.
*/
--surface-light-bg: oklch(0.9776 0.0066 286.28); /* #F7F7FC */
--surface-light-1: oklch(1 0 0); /* #FFFFFF */
--surface-light-2: oklch(0.9601 0.0093 286.22); /* #F1F1F8 */
--surface-light-3: oklch(0.9339 0.0134 286.14); /* #E8E8F2 */
--surface-light-4: oklch(0.8916 0.0149 286.09); /* #DADAE5 */
--surface-light-input: oklch(0.62 0.0149 286.09); /* #85858F 3.07:1 on light active */
--surface-light-active: oklch(0.942 0.0256 293.15); /* #ECE9FC */
--surface-dark-bg: oklch(0.1395 0.0048 262.8); /* #08090B */
--surface-dark-1: oklch(0.1588 0.0045 264.44); /* #0C0D0F */
--surface-dark-2: oklch(0.1865 0.0044 264.46); /* #121315 */
--surface-dark-3: oklch(0.1886 0.008 285.6); /* #131317 */
--surface-dark-4: oklch(0.2044 0.0043 264.47); /* #161719 */
--surface-dark-active: oklch(0.2396 0.019 284.87); /* #1E1E28 */
/*
* Window chrome — the surface the whole console is mounted on.
*
* Deliberately NOT a rung of the ladder above. Those six are *content*
* surfaces: the canvas a page is drawn on and the panels that stack over
* it. This one sits behind all of them — it is the frame the sidebar
* stands on and the margin the content card floats in (issue #1178), and
* the ladder has no rung for "further back than the canvas".
*
* Light needs a value the ladder does not carry. It has to be darker than
* the canvas (#F7F7FC) by enough to read as a separate layer, and light
* enough that `--ink-light-muted` still clears 4.5:1 on it, since the
* sidebar's faintest labels sit here. Rung 2 (#F1F1F8) separates less than
* today's white sidebar does; rung 3 (#E8E8F2) drops muted ink to 4.44:1.
* #EBEBF4 is between them: 1.11:1 against the card, 4.57:1 for muted ink.
*
* Dark takes rung 2 as-is and inverts the direction — the canvas is
* already the darkest value in the theme, so "further back" cannot mean
* darker there. #030405 against #08090B measures 1.03:1, which is not a
* layer, it is a rendering artefact. Stepping *up* to #121315 gives
* 1.07:1, and the card's hairline and inset highlight carry the rest.
*/
--surface-light-chrome: oklch(0.9427 0.012 286.17); /* #EBEBF4 */
/*
* Ink — the five text levels, stated per theme.
*
* `primary` and the light `secondary` are the guide's values. The rest are
* the guide's hue and chroma at a corrected lightness, because six of the
* ten levels as drawn do not clear 4.5:1 against the very surfaces the
* guide assigns them — dark `tertiary`, which it marks as *body text*,
* measured 2.91:1, and light `muted` measured 1.45:1.
*
* Correcting each one by the smallest lift that clears the floor collapses
* the ramp: every level lands on 4.5:1 and the five levels become one
* colour. So the ramp is redistributed instead — the weakest level sits on
* the floor and the rest step up from it in even increments of L, which is
* the same perceptual-uniformity argument the brand ramp above is built
* on. Hierarchy is preserved by *role* (secondary strongest, muted
* weakest); the guide draws `hint` brighter than `secondary` in dark,
* which its own usage labels contradict.
*
* Two levels also move hue. Dark `hint` (#8E9286) and dark `muted`
* (#62665C) are drawn at ~122° — green — while every other neutral in the
* guide sits near 285°, and the guide's own subtitle reads "All cool-toned
* greys with purple undertone". They are brought back to that.
*
* Ratios below are worst-case across the three grounds text actually sits
* on (bg, surface 1, active); rungs 2–4 are strokes and icon circles, not
* text grounds. See `docs/design-system/color.md` for the full table.
*/
--ink-light-primary: oklch(0.1505 0.0214 283.53); /* #0A0A14 16.53:1 */
--ink-light-secondary: oklch(0.391 0.0388 284.77); /* #43435A 8.05:1 */
--ink-light-tertiary: oklch(0.436 0.0165 244.9); /* #4A535A 6.59:1 */
--ink-light-hint: oklch(0.481 0.0131 259.82); /* #5A5E65 5.47:1 */
--ink-light-muted: oklch(0.526 0.0155 286.04); /* #6A6973 4.54:1 */
--ink-dark-primary: oklch(1 0 0); /* #FFFFFF 16.52:1 */
--ink-dark-secondary: oklch(0.754 0.0185 285.78); /* #AEAEBB 7.53:1 */
--ink-dark-tertiary: oklch(0.709 0.0181 248.18); /* #99A2AC 6.38:1 */
--ink-dark-hint: oklch(0.664 0.018 285.78); /* #92929E 5.37:1 */
--ink-dark-muted: oklch(0.619 0.0163 285.78); /* #858590 4.53:1 */
/* Status hues.
Each ships in three weights because one value cannot do all three
jobs: `-mark` paints dots, bars and fills (needs 3:1 against the
canvas), `-text` sets words (needs 4.5:1, so it is materially darker
in light mode), and `-soft` is the tinted background a badge sits on.
Every ratio is measured, not eyeballed — see docs/design-system/color.md. */
--green-mark: oklch(0.60 0.1627 151.05); /* #009A49 3.08:1 on light active */
--green-text: oklch(0.5175 0.1343 151.56); /* #0A7D3E 5.10:1 */
--green-bright: oklch(0.7369 0.1603 156.57); /* #35C77F dark mode */
--amber-mark: oklch(0.62 0.1585 72.33); /* #BF7200 3.14:1 on light active */
--amber-text: oklch(0.5538 0.1207 66.44); /* #A16207 4.80:1 */
--amber-bright: oklch(0.8537 0.1572 84.13); /* #FFC53D dark mode */
--red-mark: oklch(0.6256 0.1933 23.03); /* #E5484D 3.29:1 on light active */
--red-text: oklch(0.5409 0.1916 25.1); /* #C62A2F 5.43:1 */
--red-bright: oklch(0.7013 0.1901 21.2); /* #FF6369 dark mode */
--cyan-mark: oklch(0.61 0.1479 237.32); /* #008DD0 3.08:1 on light active */
--cyan-text: oklch(0.5103 0.1085 236.98); /* #0A6E9C 5.50:1 */
--cyan-bright: oklch(0.7535 0.139 232.66); /* #38BDF8 dark mode */
--pink-mark: oklch(0.6341 0.213 1.28); /* #E93D82 */
--pink-bright: oklch(0.7261 0.1884 357.98); /* #FF6BA6 */
/* ------------------------------------------------------------------
* LAYER 2 — SEMANTICS (light)
* ---------------------------------------------------------------- */
/* Surfaces, from furthest back to closest to the operator. The canvas
is a hair off white and cards are pure white, so a card lifts off the
page by lightness alone — before any shadow is applied. */
--background: var(--surface-light-bg);
--foreground: var(--ink-light-primary);
--card: var(--surface-light-1);
--card-foreground: var(--ink-light-primary);
--popover: var(--surface-light-1);
--popover-foreground: var(--ink-light-primary);
/* Interaction. `primary` is the brand: the button you press, the link
you follow, the series that leads a chart. */
--primary: var(--brand-500);
--primary-foreground: var(--surface-light-1);
--secondary: var(--surface-light-3);
--secondary-foreground: var(--ink-light-primary);
--muted: var(--surface-light-2);
--muted-foreground: var(--ink-light-muted);
/* The five-level text hierarchy the brand guide defines, named directly
so a component can ask for the rung it means instead of approximating
it with `foreground` and `muted-foreground`. `--ink-primary` and
`--foreground` are deliberately the same value: one is the hierarchy's
top rung, the other is the shadcn contract, and collapsing them would
break one of the two vocabularies. */
/* Rung 3 is the ground behind an icon circle. It is the only rung the
shadcn vocabulary has no name for, so it gets one here rather than
being approximated with `muted`. */
--surface-icon: var(--surface-light-3);
--ink-primary: var(--ink-light-primary);
--ink-secondary: var(--ink-light-secondary);
--ink-tertiary: var(--ink-light-tertiary);
--ink-hint: var(--ink-light-hint);
--ink-muted: var(--ink-light-muted);
/* `accent` is the hover/rest tint under menu rows, list items and
nav. Brand-tinted, but the *text* on it stays neutral — 40 call
sites pair `bg-accent` with `text-accent-foreground`, and indigo
text on every hover would make the console strobe. The one place
brand ink is allowed is the active sidebar row, below. */
--accent: var(--surface-light-active);
--accent-foreground: var(--ink-light-primary);
--destructive: var(--red-text);
--destructive-foreground: var(--surface-light-1);
--border: var(--surface-light-4);
--input: var(--surface-light-input);
/* The window chrome, and the hairline where the content card meets it.
One value, painted exactly once — on the shell root — so the sidebar
column and the frame around the card are the same continuous surface.
Tinting them separately is what puts the seam back. See
`components/content-surface.tsx`. */
--chrome: var(--surface-light-chrome);
--chrome-border: var(--surface-light-4);
/* Focus is branded. It is the most-seen interactive signal in the app
and it was grey. */
--ring: var(--brand-500);
/* Run status — the vocabulary this product actually needs.
An agent run is idle, running, waiting on a human, done, or failed;
those five map to five fixed colours everywhere they appear, so a
green dot means the same thing in the Overview, a task card and a
run log. `-soft` backs the badge, `-text` sets its label. */
--status-idle: var(--ink-light-hint);
--status-idle-text: var(--ink-light-muted);
--status-idle-soft: var(--surface-light-2);
--status-running: var(--cyan-mark);
--status-running-text: var(--cyan-text);
--status-running-soft: color-mix(in oklab, var(--cyan-mark) 12%, transparent);
--status-blocked: var(--amber-mark);
--status-blocked-text: var(--amber-text);
--status-blocked-soft: color-mix(in oklab, var(--amber-mark) 16%, transparent);
--status-done: var(--green-mark);
--status-done-text: var(--green-text);
--status-done-soft: color-mix(in oklab, var(--green-mark) 12%, transparent);
--status-failed: var(--red-mark);
--status-failed-text: var(--red-text);
--status-failed-soft: color-mix(in oklab, var(--red-mark) 12%, transparent);
/*
* Identity tones.
*
* A categorical palette for *who*, not *what state*: the tile behind a
* desk's initials, a teammate's avatar, a thread's tint. Assigned by
* hash, so a name keeps its colour across reloads and carries no meaning
* beyond "not the other one".
*
* Deliberately avoids amber, green and red. Identity used to be drawn
* from the same Tailwind palette as status, which meant a desk could be
* tinted the exact green that means "done" and another the red that
* means "failed" — the collision the brand doc warns about, sitting in
* the product. These five hues stay clear of the three status colours
* where a misread actually costs something.
*
* Form separates them from the rest: identity is a *tile with initials*,
* status is a *pill or dot with a label*. They never take the same shape.
*/
--tone-1: oklch(0.6056 0.2189 292.72); /* #8B5CF6 violet */
--tone-1-text: oklch(0.4907 0.2412 292.58); /* #6D28D9 6.93:1 */
--tone-2: oklch(0.6231 0.188 259.81); /* #3B82F6 blue */
--tone-2-text: oklch(0.4882 0.2172 264.38); /* #1D4ED8 6.54:1 */
--tone-3: oklch(0.7038 0.123 182.5); /* #14B8A6 teal */
--tone-3-text: oklch(0.5109 0.0861 186.39); /* #0F766E 5.34:1 */
--tone-4: oklch(0.6668 0.2591 322.15); /* #D946EF fuchsia */
--tone-4-text: oklch(0.518 0.2258 323.95); /* #A21CAF 6.17:1 */
--tone-5: oklch(0.5544 0.0407 257.42); /* #64748B slate */
--tone-5-text: oklch(0.4455 0.0374 257.28); /* #475569 7.39:1 */
/*
* Search-match highlight — the ground behind a `<mark>`.
*
* Its own token rather than a status one. A matched substring is not a
* state: it is not blocked, and routing it through `--status-blocked`
* would put the colour that means "a human must act" behind every search
* result. Yellow because that is what a highlighter is, and the one place
* this system spends a hue on convention rather than meaning.
*
* Text on it stays `--foreground`: the mark tints the ground and never
* recolours the words, so an excerpt reads the same highlighted or not.
*/
--highlight: oklch(0.9 0.16 100 / 55%);
/* Charts. Brand leads slot 1, then the sequence walks the hue circle
so neighbouring series never collide. Ordered by how often a series
count occurs: the two-series case gets indigo/cyan, which are also
the pair that survives the most common colour-vision deficiencies. */
--chart-1: var(--brand-500);
--chart-2: var(--cyan-mark);
--chart-3: var(--green-mark);
--chart-4: var(--amber-mark);
--chart-5: var(--pink-mark);
/* Sidebar. Slightly recessed from the canvas so the working area is
the brightest thing on screen. */
--sidebar: var(--surface-light-1);
--sidebar-foreground: var(--ink-light-primary);
--sidebar-primary: var(--brand-500);
--sidebar-primary-foreground: var(--surface-light-1);
/* Brand 100, not the `active` rung. The rung was tuned to read on a white
sidebar; the column sits on `--chrome` now (#EBEBF4), and #ECE9FC
against that measures 1.02:1 — the selected row simply vanished. Brand
100 is the same tint one step deeper: 1.10:1 on the chrome, and
`--sidebar-accent-foreground` still measures 6.29:1 on it. */
--sidebar-accent: var(--brand-100);
/* The one place brand ink is spent: the nav row you are standing on. */
--sidebar-accent-foreground: var(--brand-700);
--sidebar-border: var(--surface-light-4);
--sidebar-ring: var(--brand-500);
/* Corner radius. 10px at `lg` — the whole scale derives from it, so a
single edit re-rounds the app. */
--radius: 0.625rem;
/* Elevation. Tinted with the neutral hue rather than pure black:
a black shadow over a hue-tinted surface goes muddy. Four steps,
each pairing a tight contact shadow with a wider ambient one.
`xs` is a hairline lift for inputs; `lg` is for anything that
floats over the page (dialogs, popovers, command palette). */
--shadow-color: 285deg 25% 12%;
--shadow-xs: 0 1px 2px 0 hsl(var(--shadow-color) / 0.05);
--shadow-sm: 0 1px 2px -1px hsl(var(--shadow-color) / 0.08),
0 1px 3px 0 hsl(var(--shadow-color) / 0.06);
--shadow-md: 0 2px 4px -2px hsl(var(--shadow-color) / 0.08),
0 4px 12px -2px hsl(var(--shadow-color) / 0.08);
--shadow-lg: 0 4px 8px -4px hsl(var(--shadow-color) / 0.1),
0 12px 32px -8px hsl(var(--shadow-color) / 0.12);
--shadow-xl: 0 8px 16px -8px hsl(var(--shadow-color) / 0.12),
0 24px 56px -12px hsl(var(--shadow-color) / 0.16);
/* A brand-tinted glow for the focused/selected state on a raised
surface. Not a substitute for the focus ring — an addition to it. */
--shadow-brand: 0 2px 8px -2px color-mix(in oklab, var(--brand-500) 32%, transparent);
/* The ink of the orbiting halo behind the content card (`.content-orbit`).
Dark, because the effect is a drifting *shadow* rather than a light: on
the light chrome it reads as the card being lifted at one edge.
`--shadow-color` is the same hue every other shadow in this file uses, so
the halo belongs to the same family rather than being a second opinion
about what a shadow looks like here. */
--orbit-ink: hsl(var(--shadow-color) / 0.5);
/* A soft brand glow bleeding from a card's lower-left corner, for the
standing invitations on an otherwise empty surface — the "create a
teammate" / "add people" pair on a channel with no transcript.
A token rather than a gradient written at the call site because a glow
is a *colour*, and a colour that cannot theme is wrong in one of the two
modes: the value below is barely there against a light canvas, and the
`.dark` counterpart has to work several times harder to lift a card off
near-black. One utility, two tunings, no call site that has to know
which mode it is in. */
--glow-brand-card: radial-gradient(
120% 108% at 14% 112%,
color-mix(in oklab, var(--brand-500) 13%, transparent) 0%,
transparent 62%
);
/*
* Third-party brand colours.
*
* The one legitimate reason to pin a hex: these are not ours to choose.
* Discord's blurple identifies Discord, and a themed approximation of it
* would be wrong in both themes. Named here rather than inlined at the
* four call sites so the intent — "this is deliberately not our palette"
* — is legible, and so a future provider row has somewhere to live.
*/
--brand-discord: #5865f2;
--brand-discord-hover: #4752c4;
--brand-discord-on-dark: #7f8ffa;
/* Discord's own darker blurple, for the blurple *as text* on a light
ground. The 500 is a fill colour: white on it measures 4.70:1, but the
label set in it on the light sidebar measures 4.20:1 — under the 4.5
a 14px label needs. This is still Discord's palette, which is the
constraint that matters; it is only a different step of it. */
--brand-discord-on-light: #4752c4;
/* Chargebee's orange, from their media kit's symbol mark. One value, not a
per-theme pair like the blurple above: as a graphic it measures 5.36:1 on
the dark canvas and 3.67:1 on the light one, both clear of the 3:1 a
non-text mark needs, so neither theme has to bend it. Their navy (#012A38)
and white variants each vanish into one of the two. */
--brand-chargebee: #ff3300;
/* PayPal's monogram, which is three colours rather than one: the back P,
the front P, and the region where they cross.
Both P colours move with the theme, and each has to. The back P at
#002991 on the dark card measures 1.61:1 — under the 3:1 a mark needs,
and far enough under that it stops reading as a letter and starts
reading as a hole. #008cff is PayPal's own next step up and clears it
at 5.74:1. The front P is the mirror case on the other ground: #60cdff
on the white card measures 1.80:1, so #0070ba is its light-theme value
and clears the same bar at 5.22:1. Same argument as
`--brand-discord-on-light` above: still their palette, only a different
step of it.
The crossing region needs a darker light-theme value so the overlap
still reads as depth over the front P: #0066a8 is darker than #0070ba
and clears 3:1 on the white card at 6.06:1. On dark it uses #008cff,
the back P's own value, so it retains the existing treatment there. */
--brand-paypal-back: #002991;
--brand-paypal-back-on-dark: #008cff;
--brand-paypal-front: #0070ba;
--brand-paypal-front-on-dark: #60cdff;
--brand-paypal-overlap: #0066a8;
--brand-paypal-overlap-on-dark: #008cff;
}
.dark {
/* Surfaces climb *toward* the viewer with lightness, the same
direction as light mode. The canvas is near-black, and each layer
above it is a measured step up — never a translucent white wash,
which tints unpredictably over whatever it lands on. */
--background: var(--surface-dark-bg);
--foreground: var(--ink-dark-primary);
--card: var(--surface-dark-1);
--card-foreground: var(--ink-dark-primary);
--brand-paypal-back: #008cff;
--brand-paypal-front: #60cdff;
--brand-paypal-overlap: #008cff;
/* Rung 3, not rung 4. `--border` is rung 4, and a popover painted rung 4
renders its own edge in its own fill — the dialog loses its outline
entirely. Rung 3 keeps the popover above the card (rung 1) and leaves
the stroke a step to read against. */
--popover: var(--surface-dark-3);
--popover-foreground: var(--ink-dark-primary);
/* Brand-500 is too dense to read as ink on near-black; 400 is the
working accent in dark mode. Filled brand buttons keep 500 with
white text, which measures 4.70:1 in both themes. */
--primary: var(--brand-400);
--primary-foreground: var(--surface-dark-bg);
--secondary: var(--surface-dark-active);
--secondary-foreground: var(--ink-dark-primary);
--muted: var(--surface-dark-2);
--muted-foreground: var(--ink-dark-muted);
--surface-icon: var(--surface-dark-3);
--ink-primary: var(--ink-dark-primary);
--ink-secondary: var(--ink-dark-secondary);
--ink-tertiary: var(--ink-dark-tertiary);
--ink-hint: var(--ink-dark-hint);
--ink-muted: var(--ink-dark-muted);
--accent: var(--surface-dark-active);
--accent-foreground: var(--ink-dark-primary);
--destructive: var(--red-bright);
--destructive-foreground: var(--surface-dark-bg);
/* Borders were translucent white here, so that one value could read
against the canvas, the card and the popover at once. The guide names
the stroke outright — rung 4, "card stroke / dividers" — so they are
opaque now, and the ladder is what keeps them legible: every text
ground (bg, 1, active) sits clear of rung 4 in lightness. `--input`
takes the active rung, one step up, so a field edge stays visible
against a card that already uses rung 4 as its own border. */
--border: var(--surface-dark-4);
--input: var(--surface-dark-active);
--ring: var(--brand-400);
/* Rung 2, one step *up* from the canvas — see `--surface-light-chrome`
for why the direction inverts here. The hairline takes the active rung
rather than rung 4: rung 4 is only 1.04:1 against this ground, and a
card edge nobody can see is the seam problem in reverse. */
--chrome: var(--surface-dark-2);
--chrome-border: var(--surface-dark-active);
--status-idle: var(--ink-dark-muted);
--status-idle-text: var(--ink-dark-muted);
--status-idle-soft: oklch(1 0 0 / 6%);
--status-running: var(--cyan-bright);
--status-running-text: var(--cyan-bright);
--status-running-soft: color-mix(in oklab, var(--cyan-bright) 16%, transparent);
/* A dark halo on near-black chrome is invisible, so dark mode lifts the
ink rather than dropping the effect — the same inversion `--shadow-*`
already makes when its drop shadow becomes an inset highlight. Still
faint: this is a hint of a rim, not a neon edge. */
--orbit-ink: color-mix(in oklab, var(--foreground) 22%, transparent);
--status-blocked: var(--amber-bright);
--status-blocked-text: var(--amber-bright);
--status-blocked-soft: color-mix(in oklab, var(--amber-bright) 16%, transparent);
--status-done: var(--green-bright);
--status-done-text: var(--green-bright);
--status-done-soft: color-mix(in oklab, var(--green-bright) 16%, transparent);
--status-failed: var(--red-bright);
--status-failed-text: var(--red-bright);
--status-failed-soft: color-mix(in oklab, var(--red-bright) 16%, transparent);
--chart-1: var(--brand-400);
--chart-2: var(--cyan-bright);
--chart-3: var(--green-bright);
--chart-4: var(--amber-bright);
--chart-5: var(--pink-bright);
/* Identity tones: the mark holds, the ink lifts. On near-black the
light-mode text weights read as bruises, so each takes its bright
counterpart — measured 10.6–13.2:1 against the canvas. */
--tone-1-text: oklch(0.8112 0.1013 293.57); /* #C4B5FD */
--tone-2-text: oklch(0.8091 0.0956 251.81); /* #93C5FD */
--tone-3-text: oklch(0.8549 0.1251 181.07); /* #5EEAD4 */
--tone-4-text: oklch(0.833 0.1322 321.43); /* #F0ABFC */
--tone-5-text: oklch(0.869 0.0198 252.89); /* #CBD5E1 */
/* Dimmer and more saturated on near-black: the light value washes out to
a grey smudge over the dark canvas. */
--highlight: oklch(0.78 0.17 98 / 32%);
--sidebar: var(--surface-dark-1);
--sidebar-foreground: var(--ink-dark-primary);
--sidebar-primary: var(--brand-400);
--sidebar-primary-foreground: var(--surface-dark-bg);
--sidebar-accent: var(--surface-dark-active);
--sidebar-accent-foreground: var(--brand-300);
--sidebar-border: var(--surface-dark-4);
--sidebar-ring: var(--brand-400);
/* Shadow alone cannot separate two near-black surfaces — there is no
light for it to occlude. Dark mode adds a 1px inset top highlight
instead, which is what actually reads as "this edge is raised". */
--shadow-color: 285deg 60% 2%;
--shadow-xs: 0 1px 2px 0 hsl(var(--shadow-color) / 0.4),
inset 0 1px 0 0 oklch(1 0 0 / 3%);
--shadow-sm: 0 1px 2px -1px hsl(var(--shadow-color) / 0.5),
0 1px 3px 0 hsl(var(--shadow-color) / 0.4), inset 0 1px 0 0 oklch(1 0 0 / 4%);
--shadow-md: 0 2px 4px -2px hsl(var(--shadow-color) / 0.5),
0 4px 12px -2px hsl(var(--shadow-color) / 0.45), inset 0 1px 0 0 oklch(1 0 0 / 5%);
--shadow-lg: 0 4px 8px -4px hsl(var(--shadow-color) / 0.5),
0 12px 32px -8px hsl(var(--shadow-color) / 0.55), inset 0 1px 0 0 oklch(1 0 0 / 6%);
--shadow-xl: 0 8px 16px -8px hsl(var(--shadow-color) / 0.55),
0 24px 56px -12px hsl(var(--shadow-color) / 0.6), inset 0 1px 0 0 oklch(1 0 0 / 7%);
--shadow-brand: 0 2px 12px -2px color-mix(in oklab, var(--brand-400) 40%, transparent);
/* The light value is invisible here — `--brand-400` at roughly twice the
mix is what actually separates the card from the canvas. See `:root`. */
--glow-brand-card: radial-gradient(
120% 108% at 14% 112%,
color-mix(in oklab, var(--brand-400) 22%, transparent) 0%,
transparent 62%
);
}
/* ============================================================================
* LAYER 3 — UTILITIES
*
* `@theme inline` for anything whose value changes between light and dark:
* `inline` keeps the utility pointing at the variable, so the `.dark`
* override above reaches it. A plain `@theme` would bake the light value in
* at build time and dark mode would silently do nothing.
* ========================================================================== */
@theme inline {
--font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
--font-heading: var(--font-sans);
/* Data type. Numbers that change in place — token counts, durations,
run ids, timestamps — must not reflow as their digits change.
Geist Mono is picked up automatically if the fontsource package is
installed; until then this falls through to the platform mono. */
--font-mono: "Geist Mono Variable", ui-monospace, "SF Mono", "Menlo",
monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chrome: var(--chrome);
--color-chrome-border: var(--chrome-border);
--color-surface-icon: var(--surface-icon);
/* The five-level text hierarchy, as `text-ink-*`. Named `ink` rather than
`text` because `--text-*` is Tailwind's font-size namespace in this same
block — `--text-primary` here would mint a font size called
`text-primary`, silently. */
--color-ink-primary: var(--ink-primary);
--color-ink-secondary: var(--ink-secondary);
--color-ink-tertiary: var(--ink-tertiary);
--color-ink-hint: var(--ink-hint);
--color-ink-muted: var(--ink-muted);
/* The brand ramp, addressable directly for the rare case that needs a
specific step (illustration, a marketing surface, a chart annotation).
Prefer the semantic names above for anything interactive. */
--color-brand-50: var(--brand-50);
--color-brand-100: var(--brand-100);
--color-brand-200: var(--brand-200);
--color-brand-300: var(--brand-300);
--color-brand-400: var(--brand-400);
--color-brand-500: var(--brand-500);
--color-brand-600: var(--brand-600);
--color-brand-700: var(--brand-700);
--color-brand-800: var(--brand-800);
--color-brand-900: var(--brand-900);
--color-status-idle: var(--status-idle);
--color-status-idle-text: var(--status-idle-text);
--color-status-idle-soft: var(--status-idle-soft);
--color-status-running: var(--status-running);
--color-status-running-text: var(--status-running-text);
--color-status-running-soft: var(--status-running-soft);
--color-status-blocked: var(--status-blocked);
--color-status-blocked-text: var(--status-blocked-text);
--color-status-blocked-soft: var(--status-blocked-soft);
--color-status-done: var(--status-done);
--color-status-done-text: var(--status-done-text);
--color-status-done-soft: var(--status-done-soft);
--color-status-failed: var(--status-failed);
--color-status-failed-text: var(--status-failed-text);
--color-status-failed-soft: var(--status-failed-soft);
--color-tone-1: var(--tone-1);
--color-tone-1-text: var(--tone-1-text);
--color-tone-2: var(--tone-2);
--color-tone-2-text: var(--tone-2-text);
--color-tone-3: var(--tone-3);
--color-tone-3-text: var(--tone-3-text);
--color-tone-4: var(--tone-4);
--color-tone-4-text: var(--tone-4-text);
--color-tone-5: var(--tone-5);
--color-tone-5-text: var(--tone-5-text);
--color-highlight: var(--highlight);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-brand: var(--shadow-brand);
/* `--background-image-*` is the namespace that generates a `bg-*` utility
for a gradient, the way `--color-*` does for a flat fill. Gives
`bg-glow-brand-card`. */
--background-image-glow-brand-card: var(--glow-brand-card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
/* Static scales — identical in both themes, so no `inline` needed. */
@theme {
/* Type scale.
*
* Tailwind's own steps (xs 12 / sm 14 / base 16 …) are untouched, because
* 460 call sites already depend on them. What is added is the two steps
* below `xs` that this console genuinely needs and had been spelling as
* arbitrary values — `text-[11px]` appears 109 times and `text-[10px]` 50
* times. Those are not one-offs; they are the two densest rungs of the
* real scale, and they belong in it.
*
* text-3xs 10px — dense table meta, graph node labels, badge counters
* text-2xs 11px — the console's caption size: timestamps, key/value
* rows, sidebar section headers
*
* Below 10px is not a size, it is a bug. Nothing smaller is defined, and
* the audit in docs/design-system/typography.md lists the six sites
* currently under it that need raising.
*/
--text-3xs: 0.625rem; /* 10px */
--text-3xs--line-height: 0.875rem; /* 14px */
--text-3xs--letter-spacing: 0.01em;
--text-2xs: 0.6875rem; /* 11px */
--text-2xs--line-height: 1rem; /* 16px */
--text-2xs--letter-spacing: 0.005em;
/* Motion curves.
*
* `--ease-*` is a Tailwind theme namespace, so these both emit as
* variables *and* generate `ease-standard` / `ease-emphasized`
* utilities. `--ease-emphasized` is the curve the knowledge graph
* already used; it is promoted here so the rest of the app can share
* it instead of re-declaring it. It decelerates hard, which is what
* makes a panel feel like it settled rather than stopped.
*/
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-emphasized: cubic-bezier(0.32, 0.72, 0, 1);
}
/*
* Motion durations.
*
* Declared at `:root` rather than in `@theme`, and that is not a style
* choice. Tailwind v4 only emits `@theme` entries that fall in a namespace
* it recognises; `--duration-*` is not one, so declaring them there defines
* nothing at runtime and every `var(--duration-fast)` silently resolves to
* an empty value — a transition that simply never runs. Verified against the
* built stylesheet, not assumed.
*
* Three durations, and that is the whole vocabulary. `fast` is for state
* that must feel instant under the cursor (hover, press). `base` is the
* default for anything entering or leaving. `slow` is reserved for a surface
* large enough that a fast move would read as a jump — a sheet, a dialog,
* the sidebar collapsing.
*/
/* ============================================================================
* The app frame — how far the content card is inset from the window chrome
* (issue #1178).
*
* One quantity, spent on all four sides by `ContentSurface`, so the frame is
* even by construction rather than by four numbers that happen to agree.
*
* It was briefly a special case on top, aligned to the sidebar's header block
* and measured at runtime to stay aligned. That is gone, along with the
* measurement that fed it: an even frame is what this needs, and a mechanism
* kept for a rule that no longer exists is a thing that rots.
* ========================================================================== */
:root {
--frame-inset: 0.75rem;
}
:root {
--duration-fast: 120ms;
--duration-base: 180ms;
--duration-slow: 260ms;
}
/* ============================================================================
* Knowledge-graph bridge.
*
* The Overview's graph was ported with its own palette vocabulary. Rather
* than rename ~2000 lines of it, its names are re-pointed at the semantic
* layer here. Two consequences worth knowing:
*
* - The graph now themes for free. It used to carry its own hardcoded
* light/dark hex pairs; those are gone.
* - The mapping is one-way. Nothing outside `.oc-kg` may use these names.
*
* The hues stay at `:root` rather than moving inside `.oc-kg`, because a
* `@theme` entry pointing at a variable that only exists deeper in the tree
* does not register at build time — the utility silently falls back to
* `currentColor`, which paints every frame stark white.
* ========================================================================== */
:root {
--kg-accent: var(--brand-500);
--kg-ok: var(--status-done);
--kg-warn: var(--status-blocked);
/* The two brain hues stay deliberately outside the status vocabulary:
they identify *which* store a node came from, and colouring them with
status hues would imply a health that they do not carry. */
--kg-brain-1: var(--pink-mark);
--kg-brain-2: var(--cyan-mark);
/* A workflow stage is declared structure, not a finished run, so it takes
an identity hue rather than a status one — the palette contract in
docs/brand/README.md keeps amber, green and red for the five run states.
tone-2 blue sits clear of the graph's cyan workflows and violet
AI teammates in both themes. */
--kg-stage: var(--tone-2);
/* The travelling spark that fires along a memory link. A lightened
brain-1 rather than its own hue, so the constellation stays one
colour family however brain-1 is retuned. */
--kg-spark: color-mix(in oklab, var(--kg-brain-1) 55%, white);
}
.dark {
--kg-accent: var(--brand-400);
--kg-ok: var(--status-done);
--kg-warn: var(--status-blocked);
--kg-brain-1: var(--pink-bright);
--kg-brain-2: var(--cyan-bright);
--kg-stage: var(--tone-2);
--kg-spark: color-mix(in oklab, var(--kg-brain-1) 65%, white);
}
@theme inline {
--color-os-bg: var(--background);
--color-os-surface: var(--card);
--color-os-border: var(--border);
--color-os-border-strong: var(--input);
--color-os-text: var(--foreground);
--color-os-muted: var(--muted-foreground);
--color-os-dim: var(--muted-foreground);
--color-os-accent: var(--kg-accent);
--color-os-ok: var(--kg-ok);
--color-os-warn: var(--kg-warn);
--color-os-err: var(--destructive);
/* The graph's own corner scale: barely rounded, so the diagram chrome
stays distinct from the console's rounded cards. */
--radius-sm-t: 2px;
--radius-md-t: 4px;
--radius-lg-t: 6px;
}
/*
* The unprefixed names the ported graph writes inline as `var(--…)`. Scoped to
* the graph so a second set of colour names never leaks into the rest of the
* app. `--border` is deliberately absent: the console already defines it, and
* aliasing it to its own `--color-border` alias would be a cycle.
*/
.oc-kg {
--ease: var(--ease-emphasized);
--bg: var(--background);
--surface: var(--card);
--border-strong: var(--input);
--text: var(--foreground);
--text-2: var(--muted-foreground);
--text-3: var(--muted-foreground);
--muted: var(--muted-foreground);
--dim: var(--muted-foreground);
--accent: var(--kg-accent);
--accent-soft: color-mix(in oklab, var(--kg-accent) 12%, transparent);
--accent-line: color-mix(in oklab, var(--kg-accent) 30%, transparent);
--ok: var(--kg-ok);
--warn: var(--kg-warn);
--err: var(--destructive);
--brain-1: var(--kg-brain-1);
--brain-2: var(--kg-brain-2);
--stage: var(--kg-stage);
--kg-tool: var(--kg-brain-2);
}
/* ============================================================================
* Markdown prose — inline code.
*
* `@plugin "@tailwindcss/typography"` at the top of this file ships two
* defaults nobody here asked for:
*
* .prose :where(code)…::before { content: "`" }
* .prose :where(code)…::after { content: "`" }
*
* They exist because the plugin styles hand-written HTML, where a reader has
* no other cue that a span is code. What `components/markdown.tsx` feeds it is
* the *output* of a markdown parser, so the fences have already been consumed:
* the plugin drew them back on, and every surface using the shared `Markdown`
* component — chat, memory, ledgers, artifacts, workflows — rendered
* `` `UsageDto` `` where it meant `UsageDto` (issue #1108).
*
* What makes the override stick is the cascade *layer*, not the selector:
* `prose` is a utility, so everything the plugin emits lands in
* `@layer utilities`, and an unlayered rule outranks every layer no matter
* where it sits in the file. So these rules must stay unlayered — wrapping
* them in `@layer base` or `@layer utilities` to tidy the file up hands the
* backticks straight back. `test/unit/markdown-inline-code.test.ts` compiles
* this file and checks the outcome rather than the text, so that mistake
* fails a test instead of shipping.
* ========================================================================== */
.prose :where(code):not(:where([class~="not-prose"] *))::before,
.prose :where(code):not(:where([class~="not-prose"] *))::after {
content: none;
}
/* Without the backticks an inline span is set apart from the prose around it
* by its face alone, which at 0.875em is a weak signal in a chat line. It gets
* its own ground instead: `--muted` with a hairline, so the chip still reads
* where whatever is behind it is itself tinted — a hovered chat row is
* `bg-muted/40`, and a `remark-gfm` table header is tinted too. Sized in `em`
* so the chip tracks the text it wraps rather than the root font size.
*
* `pre *` is excluded on purpose. The plugin clears the ground inside a fenced
* block (`pre code { background-color: transparent; padding: 0 }`), and being
* unlayered this rule would otherwise outrank that too — every fenced block
* would gain a second, bordered ground inside its own.
*/
.prose :where(code):not(:where(pre *, [class~="not-prose"] *)) {
background-color: var(--muted);
border: 1px solid var(--border);
border-radius: calc(var(--radius) * 0.6);
padding: 0.125em 0.375em;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
/* Long-form UI text at 13–14px benefits visibly from these; they are
set once here rather than per-component. */
font-feature-settings: "cv11", "ss01";
font-variation-settings: normal;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
html {
@apply font-sans;
}
/* Figures line up in columns and never reflow as a counter ticks. This
is why the mono face is scoped to data rather than applied broadly:
tabular sans covers most of the need. */
:where(table, [data-numeric]) {
font-variant-numeric: tabular-nums;
}
/* The quality floor: honour a reduced-motion preference globally rather
than remembering to do it per animation. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
/* ============================================================================
* Scrollbars (issue #1109)
*
* One rule set, at the document level, so every scroll container in the console
* — the 69 that exist and the next one somebody adds — is themed without opting
* in. The browser default is a wide, pale, permanently-on bar that belongs to
* the operating system rather than to this canvas; against `bg-card` in dark it
* reads as a light stripe down the side of the panel.
*
* Three decisions worth stating, because each had an obvious alternative:
*
* 1. THE BAR NEVER LEAVES. It rests at 22% of `--muted-foreground` (30% in
* dark) instead of `width: 0`. A scrollbar is the only thing on screen that says "there is
* more below", and hiding it until the reader scrolls means hiding it until
* after they needed it — worst on exactly the panels where the thing they
* are looking for is an action at the bottom of a list. So the fade is a
* fade in weight, not in existence.
*
* 2. THE GEOMETRY IS CONSTANT. Only the thumb's colour changes between states;
* the track keeps its width whether the thumb is bright or faint. That is
* what makes `scrollbar-gutter: stable` unnecessary here: the gutter would
* fix a reflow that this never causes, and applying it globally would carve
* a permanent 10px notch out of every `overflow-auto` box that does not
* overflow — including the tightly-sized panels (`RunHistoryPanel`,
* `NodeDetailPanel`, `CopilotPanel`) and the small `<pre>` blocks inside
* them, whose padding would go visibly lopsided.
*