snyvi 1.22.0

A fast, beautiful viewer for the documents your agents produce
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
/* snyvi — the document is the hero, the chrome recedes. */

@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url(/assets/fonts/inter.woff2) format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 400 700; font-display: swap; src: url(/assets/fonts/inter-italic.woff2) format("woff2"); }
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 600; font-display: swap; src: url(/assets/fonts/jetbrains-mono.woff2) format("woff2"); }
@font-face { font-family: "Source Serif 4"; font-style: normal; font-weight: 400 700; font-display: swap; src: url(/assets/fonts/source-serif.woff2) format("woff2"); }
@font-face { font-family: "Source Serif 4"; font-style: italic; font-weight: 400 700; font-display: swap; src: url(/assets/fonts/source-serif-italic.woff2) format("woff2"); }
@font-face { font-family: "Literata"; font-style: normal; font-weight: 200 900; font-display: swap; src: url(/assets/fonts/literata.woff2) format("woff2"); }
@font-face { font-family: "Literata"; font-style: italic; font-weight: 200 900; font-display: swap; src: url(/assets/fonts/literata-italic.woff2) format("woff2"); }
@font-face { font-family: "Atkinson Hyperlegible Next"; font-style: normal; font-weight: 200 800; font-display: swap; src: url(/assets/fonts/atkinson.woff2) format("woff2"); }
@font-face { font-family: "Atkinson Hyperlegible Next"; font-style: italic; font-weight: 200 800; font-display: swap; src: url(/assets/fonts/atkinson-italic.woff2) format("woff2"); }

/* ---------- the themes ----------
   Eight, each one block of the same names: surfaces, ink, rules, the accent
   pair and its wash, code, mark, diff, depth, status, the mascot's pink,
   ten syntax tokens and the terminal's sixteen (docs/THEMES.md, the token
   contract). Nothing draws with any other colour. Paper's values sit on
   :root and every other theme is a block of overrides on it, matched by
   `[data-theme]` alone rather than `:root[data-theme]` so that any element
   can wear one -- the rows of the ⌘K theme list are each their own swatch
   that way, and a desk will wear its own accent by the same route.
   boot.js always sets `data-theme` on <html> before this sheet applies, so
   :root alone is only ever seen when storage is unreadable, and then it is
   Paper. Every status, syntax and terminal colour in every theme is read
   back by bench/page.mjs on each of its four surfaces and the build fails
   under 4.5:1 -- 7:1 for Contrast.
   Only Paper and Ink are here, the two defaults, so a first launch in either
   light or dark opens in its own colours. The other six are in themes.css,
   fetched once the page is idle; boot.js paints a returning reader's theme
   from a copy the app kept of it, so leaving them out costs no flash. */
:root {
  --sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --serif: "Source Serif 4", ui-serif, Georgia, "Times New Roman", serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --body: var(--sans);

  /* Paper: the warm near-white, the default light theme. */
  color-scheme: light;
  --bg: #faf9f6;
  --bg-side: #f3f1ec;
  --bg-raise: #ffffff;
  --fg: #1f1d1a;
  --fg-2: #5c574f;
  --fg-3: #736c61;
  --rule: #e6e2da;
  --rule-2: #d8d3c9;
  /* The accent, picked with the swatch in the sidebar's foot. Each choice is
     a pair: -l sits on paper, -d on ink. Which half a theme wears is its
     `color-scheme`, read by light-dark() in the derivation rule below the
     accents, so a theme never names the half and the accent stays an axis
     of its own. A theme may set the pair's default (Parchment's red is
     warmer, Contrast's is lifted to clear 7:1) and a chosen accent, whose
     block comes later, overrides it. The wash is the accent over the
     surface: how much, and over what, is the theme's. */
  --accent-l: #8c0f26;
  --accent-d: #f04a63;
  --wash: 10%;
  --wash-on: var(--bg);
  --deepen: 0%;   /* how far this theme pulls every accent to black (light) */
  --lift: 0%;     /* or to white (dark), to keep it at 4.5:1 on its ground */
  --kw: 85%;   /* the keyword: the accent, and how much of it before black */
  --brand: var(--accent-l);   /* the logo's colour, the same in every theme */
  /* snyvi's face: a bright tone of the accent for the body, picked per
     accent (the default is a red that means it), the brand itself on the
     nub, and a deep shade of it for the eyes and mouth. The blush on the
     cheeks and the heart it holds up are one pink, on every theme so far. */
  --mascot: #d9203b;
  --mascot-nub: var(--brand);
  --heart: #f0607e;
  --mascot-peek: .22;   /* how far the mascot behind a note comes forward, hovered */
  --mascot-shine: #fff;   /* the light in its eyes */
  --code-bg: #f3f1ec;
  --mark: #fde68a;
  --add: #dcfce7; --add-fg: #166534;
  --del: #fee2e2; --del-fg: #991b1b;
  --shadow: 0 10px 40px rgba(30, 25, 15, .16), 0 1px 2px rgba(30, 25, 15, .08);
  --scrim: rgba(20, 16, 10, .28);   /* the dim behind a dialog */
  /* Status: a running dot, a blocked pane, an armed ✕, an alert's title.
     The blocked mark is eleven-pixel bold, small text under WCAG, so 4.5:1
     on every surface is the bar. */
  --ok: #177a3a;
  --warn: #9a4505;
  --danger: #b3261e;
  --info: #1e5fd0;
  --important: #7c3aed;
  /* Syntax. The keyword and the tag are the accent, derived below. */
  --s-comment: #736c61;
  --s-string: #3b6d11;
  --s-number: #0b6e99;
  --s-function: #6a3fb5;
  --s-type: #0e7490;
  --s-variable: #1f1d1a;
  --s-punct: #6b665e;
  --s-attr: #6a3fb5;
  /* The terminal's sixteen, for the panels on a desk: a shell is dressed
     with the page it sits in. On a light ground the bright eight are not
     brighter -- a bright yellow on paper is no yellow at all -- but a second
     set of inks that still read at 4.5:1 on the panel. */
  --t0: #1f1d1a; --t1: #b3261e; --t2: #3b6d11; --t3: #8a5a06; --t4: #1d4ed8; --t5: #7e22ce; --t6: #0e7490; --t7: #6f6a61;
  --t8: #5c574f; --t9: #c81e1e; --t10: #3f6f0a; --t11: #8a5a06; --t12: #2563eb; --t13: #9333ea; --t14: #0b6e8a; --t15: #d8d3c9;

  --side-w: 264px;
  --rail-w: 232px;
  --head-h: 52px;   /* the document's head: the brand row's height, so the panes' tops agree */
  --measure: 70ch;

  /* The scales (docs/DESIGN.md §6.2, §7.1). Components read these, never a
     number: bench/lint-ui.mjs counts every px font size, z-index and raw
     duration outside this block, and the count only goes down. Type: seven
     chrome steps; prose has its own size per reading face. */
  --fs-micro: 11px; --fs-small: 12px; --fs-ui: 13px; --fs-body-s: 14px;
  --fs-h3: 20px; --fs-h2: 26px; --fs-h1: 34px; --fs-read: 17px;
  --fw-row: 450;   /* the one named exception: sidebar rows, the most-read chrome */
  /* Space: a 4-point scale, with 10 and 20 kept as steps. */
  --sp-1: 2px; --sp-2: 4px; --sp-3: 6px; --sp-4: 8px; --sp-5: 10px; --sp-6: 12px;
  --sp-7: 16px; --sp-8: 20px; --sp-9: 24px; --sp-10: 32px; --sp-11: 48px;
  /* Radius: focus rings and kbd · rows, buttons, inputs, tips · menus,
     popovers, cards, dialogs, toasts · counts and pills. */
  --r-xs: 4px; --r-sm: 6px; --r-md: 10px; --r-pill: 999px;
  /* Layers, named. A local 0-5 inside one component is allowed. Toasts sit
     above dialogs, so an answer is never hidden by the dialog that asked. */
  --z-sticky: 10; --z-rail: 20; --z-dialog: 30; --z-pop: 40; --z-tip: 50; --z-toast: 60; --z-over: 70;
  /* Motion: instant for what happens a hundred times a day, quick by
     default, move for what travels; the spring is the mascot's alone.
     Exits take the in-curve and about a fifth less time. */
  --dur-instant: 80ms; --dur-quick: 140ms; --dur-move: 220ms; --dur-moment: 700ms;
  --ease-out: cubic-bezier(.2,.7,.2,1); --ease-in: cubic-bezier(.4,0,1,1); --ease-spring: cubic-bezier(.3,1.6,.5,1);
  --t: var(--dur-quick) var(--ease-out);
  --undo-ms: 6000ms;   /* UNDO_MS in app.js writes it; this is the default */
  /* Elevation: a raised card · a popover, menu, tip or toast · a dialog.
     The middle one is --shadow, the name every theme already sets. */
  --shadow-1: 0 1px 3px rgba(30, 25, 15, .1);
  --shadow-3: 0 18px 60px rgba(30, 25, 15, .22), 0 2px 6px rgba(30, 25, 15, .1);
}
/* Under a system asking for more contrast, Paper keeps its ground and
   darkens its lighter inks and rules to 7:1: a light theme at that bar is
   Paper with heavier ink, not a fifth name. */
@media (prefers-contrast: more) {
  [data-theme="paper"] { --fg-2: #3f3b35; --fg-3: #5a554d; --rule: #cfc9be; --rule-2: #b9b2a5; --s-comment: #5a554d; }
}

/* Ink: the deep grey-blue, the default dark theme. Written once: boot.js
   resolves "follow the system" into a concrete `data-theme` before first
   paint, so there is no media block to keep in step with this one.
   `color-scheme` is what flips the accent, the native controls and every
   `isDark()` in the scripts. */
[data-theme="ink"] {
  color-scheme: dark;
  --bg: #15181f;
  --bg-side: #111419;
  --bg-raise: #1c2029;
  --fg: rgba(255,255,255,.87);
  --fg-2: rgba(255,255,255,.62);
  --fg-3: rgba(255,255,255,.47);
  --rule: rgba(255,255,255,.08);
  --rule-2: rgba(255,255,255,.14);
  --wash: 13%;   /* the most that keeps Passion on it at 4.5:1 */
  --wash-on: transparent;
  --kw: 100%;
  --mascot-peek: .16;
  --code-bg: #1b1f28;
  --mark: rgba(253,230,138,.35);
  --add: rgba(74,222,128,.14); --add-fg: #86efac;
  --del: rgba(248,113,113,.14); --del-fg: #fca5a5;
  --shadow: 0 10px 40px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.4);
  --shadow-1: 0 1px 3px rgba(0,0,0,.4);
  --shadow-3: 0 18px 60px rgba(0,0,0,.6), 0 2px 6px rgba(0,0,0,.4);
  --scrim: rgba(0, 0, 0, .5);
  --ok: #5bd984;
  --warn: #e6a23c;
  --danger: #f87171;
  --info: #7aa2f7;
  --important: #c084fc;
  --s-comment: rgba(255,255,255,.55);
  --s-string: #a3d977;
  --s-number: #7dd3fc;
  --s-function: #c4b5fd;
  --s-type: #67e8f9;
  --s-variable: rgba(255,255,255,.87);
  --s-punct: rgba(255,255,255,.62);
  --s-attr: #c4b5fd;
  --t0: #2a2f3a; --t1: #f87171; --t2: #86c46d; --t3: #e6b450; --t4: #7aa2f7; --t5: #c792ea; --t6: #5ccfe6; --t7: #c8ccd4;
  --t8: #8b93a3; --t9: #ff8b8b; --t10: #a6e3a1; --t11: #f9e2af; --t12: #89b4fa; --t13: #f5c2e7; --t14: #94e2d5; --t15: #ffffff;
}

/* The other accents. Passion, the red the mark wears, is the default, set on :root above. */
:root[data-accent="crimson"] { --accent-l: #b91c1c; --accent-d: #f87171; --mascot: #ff5c8a; }
:root[data-accent="rose"]    { --accent-l: #be185d; --accent-d: #f472b6; --mascot: #ff7ad9; }
:root[data-accent="violet"]  { --accent-l: #7c3aed; --accent-d: #a78bfa; --mascot: #c69cff; }
:root[data-accent="blue"]    { --accent-l: #1d4ed8; --accent-d: #60a5fa; --mascot: #7ab8ff; }
:root[data-accent="teal"]    { --accent-l: #0f766e; --accent-d: #2dd4bf; --mascot: #5ef2d6; }
:root[data-accent="green"]   { --accent-l: #137639; --accent-d: #4ade80; --mascot: #7dff9e; }
:root[data-accent="graphite"] { --accent-l: #57534e; --accent-d: #d6d3d1; --mascot: #dcd7d0; }
/* What is derived from the pair, in one rule for every element allowed to
   wear an accent. A var() inside a custom property resolves on the element
   that declares it and inherits as a colour, so this has to be declared
   where the pair is set, not once on :root: a theme row in the ⌘K list
   resolves its own light-dark() by its own color-scheme, and a desk with
   its own pair (phase 4) will resolve its own accent the same way. */
:root, [data-theme] {
  --accent: light-dark(color-mix(in srgb, var(--accent-l), #000 var(--deepen)), color-mix(in srgb, var(--accent-d), #fff var(--lift)));
  --accent-bg: color-mix(in srgb, var(--accent) var(--wash), var(--wash-on));
  /* Text and marks on an accent fill: the ground, which clears 4.5:1 on
     every accent in every theme (lowest 4.96). White fails every dark one. */
  --on-accent: var(--bg);
  --focus: var(--accent);   /* the one focus ring */
  --led-glow: 0 0 6px color-mix(in srgb, var(--ok) 60%, transparent);   /* the only glow: a live LED's */
  --mascot-ink: color-mix(in srgb, var(--brand) 40%, #000);
  --s-keyword: color-mix(in srgb, var(--accent) var(--kw), #000);
  --s-tag: var(--s-keyword);
}
/* The reading face, picked with Aa. It sets documents -- prose, wherever it
   is shown -- and nothing else: code keeps its mono so columns line up, and
   the chrome keeps Inter. Mono sets the headings too, typewriter-style. */
:root[data-font="serif"] { --body: var(--serif); }
:root[data-font="literata"] { --body: "Literata", var(--serif); }
:root[data-font="atkinson"] { --body: "Atkinson Hyperlegible Next", var(--sans); }
:root[data-font="mono"] { --body: var(--mono); --head: var(--mono); }

*, *::before, *::after { box-sizing: border-box; }
html { height: 100%; }
body {
  margin: 0; height: 100%;
  background: var(--bg); color: var(--fg);
  font-family: var(--sans); font-size: var(--fs-ui); line-height: 1.5;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  font-feature-settings: "cv11", "ss01", "calt";
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: .15em; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-xs); }
::selection { background: var(--accent-bg); }

/* ---------- layout ---------- */
/* The sidebar's column is --side-c: its width, or the 44 px rail it folds
   to (below, "the rail"). */
#app {
  display: grid; height: 100%;
  grid-template-columns: var(--side-c) minmax(0, 1fr) var(--rail-w);
  grid-template-areas: "side main rail";
}
:root { --side-c: var(--side-w); }
:root[data-side="0"] { --side-c: 44px; }
:root[data-rail="0"] #app, #app:has(#rail.empty) { grid-template-columns: var(--side-c) minmax(0,1fr) 0; }
:root[data-rail="0"] #rail, #rail.empty { display: none; }


#side, #rail { background: var(--bg-side); display: flex; flex-direction: column; min-height: 0; position: relative; }
/* The panes' edges drag. The gutter straddles the seam, so it takes nothing
   from the grid, and sets the same variable the grid reads, so every rule
   that knows the pane's width follows. app.js clamps it; boot.js applies the
   stored width before first paint. Not on a sheet, which has its own width. */
.gutter { position: absolute; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 3; touch-action: none; }
#side .gutter { right: -4px; }
#rail .gutter { left: -4px; }
.gutter::after { content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 2px; background: var(--accent); opacity: 0; transition: opacity var(--t); }
.gutter:hover::after, .gutter:focus-visible::after, :root[data-resizing] .gutter::after { opacity: .6; }
.gutter:focus-visible { outline: none; }
:root[data-resizing] { cursor: col-resize; user-select: none; }
:root[data-resizing] * { transition: none !important; }
/* Narrow windows: a pane that no longer fits beside the document becomes a
   sheet over it. `data-sheet` on the root names the one that is open; app.js
   sets it only while the pane is at a sheet width, and clears it when the
   window grows past that. The scrim behind a sheet is what a tap outside
   lands on. The two buttons in #chrome exist for the same widths. */
@media (max-width: 1100px) {
  #app { grid-template-columns: var(--side-c) minmax(0,1fr) 0 !important; }
  #rail { display: none; position: fixed; inset: 0 0 0 auto; width: min(88vw, 320px); z-index: var(--z-rail); box-shadow: var(--shadow); padding: 44px 0 20px 16px; }
  #btn-rail-hide { top: 10px; left: 12px; }
  :root[data-sheet="rail"] #rail { display: flex; animation: sheet-r var(--dur-quick) var(--ease-out); }
  #chrome #btn-rail { display: inline-grid; }
  #app:has(#rail.empty) #btn-rail { display: none; }
  #rail .gutter { display: none; }
}
@keyframes sheet-l { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
@keyframes sheet-r { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
#scrim { position: fixed; inset: 0; z-index: calc(var(--z-rail) - 1); background: var(--scrim); display: none; }
:root[data-sheet] #scrim { display: block; }
/* ---------- the document's head ----------
   A bar across the top of the document pane, the brand row's height, that
   the text scrolls under: the buttons that bring a folded pane back sit in
   it, and in the window it is the strip that drags (frame.js keeps the
   window's own three buttons at the corner, and leaves them room here).
   Sticky, in the page's own colour, so at rest it is the top margin and no
   more; once the reader has scrolled, a rule at its foot is the edge the
   text disappears under, instead of a button floating over a line. */
#chrome { position: sticky; top: 0; z-index: var(--z-sticky); box-sizing: border-box; height: var(--head-h); display: flex; align-items: start; gap: 8px; padding: 14px 10px 10px; background: var(--bg); box-shadow: 0 1px 0 transparent; transition: box-shadow var(--t); }
#main.scrolled #chrome { box-shadow: 0 1px 0 var(--rule); }
#chrome .icon { display: none; }
#chrome .chrome-r { display: flex; gap: 8px; margin-left: auto; }
/* Every document and file: its name as the bar's heading, at the left, and
   the ✕ that goes back to where it was opened from at the right -- the
   panels, for one read over a desk, beside the rail it was opened from:
   the way out where the click in was. Both in the bar the
   text scrolls under, so the page gives up no room for them. The name
   rises in once the page scrolls (#main.scrolled); at the top the page's
   own <h1> is right under it, and it would say the title twice. */
#chrome .over { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; height: 28px; padding-left: 6px; }
#chrome .over[hidden] { display: none; }
#chrome .over-t { flex: 1; min-width: 0; font-size: var(--fs-body-s); font-weight: 600; color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0; transform: translateY(6px); transition: opacity var(--dur-move), transform var(--dur-move) var(--ease-out); }
#main.scrolled #chrome .over-t { opacity: 1; transform: none; }
#chrome .over-x { flex: none; display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: var(--r-xs); color: var(--fg-3); transition: background var(--t), color var(--t); }
#chrome .over-x:hover { background: var(--rule-2); color: var(--fg); }
/* A desk draws its own head, with the desk's name in it, and the pane does
   not scroll: there the bar is only its buttons, laid over the desk's
   header, which makes room for them (desk.js, .dk-head). */
:root[data-view="desk"] #main { position: relative; }
:root[data-view="desk"] #chrome { position: absolute; top: 0; left: 0; right: 0; height: auto; background: none; box-shadow: none; pointer-events: none; }
:root[data-view="desk"] #chrome .icon { pointer-events: auto; }
/* A pane folded away at a width where it is a column: its button stays, as
   the way back. Not for a rail with nothing in it. */
:root[data-rail="0"] #chrome #btn-rail { display: inline-grid; }
#app:has(#rail.empty) #btn-rail { display: none; }
/* Each pane carries the way to put it away, at its top edge: the sidebar's
   at the end of the brand row, the rail's in the corner over the grip. The
   same glyph as the button in #chrome that brings the pane back, with the
   pane's third filled while it is here and outlined once it is gone, so
   the two read as one control that flips. */
#btn-side-hide { margin-left: 2px; }
/* ---------- the rail ----------
   The sidebar folded (`\`, or the button at its top) is a 44 px column of
   its sections' icons, at every width; under 760 px it is only ever that
   (app.js sets data-side there). An icon opens its section in a popover
   beside it, #pop, which is the section's own element moved in (app.js,
   openPop): nothing is drawn twice, and every listener on #trees still
   hears it, since #pop is inside #trees. */
#rail-nav, #pop[hidden] { display: none; }
:root[data-side="0"] #rail-nav { display: flex; flex-direction: column; align-items: center; gap: 6px; flex: 1; min-height: 0; padding: 6px 0; }
:root[data-side="0"] :is(.brand-name, .bm-say, #live, #btn-search, #side > #note, #side .gutter, #trees > :not(#pop)) { display: none; }
:root[data-side="0"] .side-head { flex-direction: column; gap: 10px; padding: 14px 0 4px; }
:root[data-side="0"] #btn-side-hide { margin: 0; }
/* The mark in a box the size of the rail's icons, so it sits on their
   centre line and not 4 px off it. */
:root[data-side="0"] .side-head .brand { width: 28px; height: 28px; display: grid; place-items: center; align-self: center; margin: 0; }
:root[data-side="0"] #btn-side-hide .pane { fill: none; }
:root[data-side="0"] #trees { flex: none; padding: 0; overflow: visible; }
:root[data-side="0"] .side-foot { padding: 8px; justify-content: center; }
#rail-nav .icon { position: relative; flex: none; }
#rail-nav .icon.on { color: var(--accent); background: var(--accent-bg); }
/* On a desk, the Desks icon is where you are, as the current row is. */
:root[data-view="desk"] #rail-nav [data-pop="desks"]:not(.on) { color: var(--fg); background: var(--accent-bg); box-shadow: inset 2px 0 0 var(--accent); }
.badge { position: absolute; top: -4px; right: -6px; min-width: 15px; height: 15px; padding: 0 4px; border-radius: var(--r-pill);
  font: 600 var(--fs-micro)/15px var(--sans); text-align: center; color: var(--accent); background: var(--accent-bg); box-shadow: 0 0 0 2px var(--bg-side); }
.badge:empty { display: none; }
.badge.blk { color: var(--warn); background: color-mix(in srgb, var(--warn) 18%, var(--bg-side)); }
/* Agents connected are live, not unread: the LED says so, and the figure
   beside it is plain, so it can't be read as a second Inbox count. */
.badge.live { color: var(--fg-2); background: none; box-shadow: none; }
#rail-nav i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: .6; }
#rail-live.on i { opacity: 1; background: var(--ok); box-shadow: var(--led-glow); }
:root[data-note] #rail-note i { opacity: 1; background: var(--accent); }
#rail-note { display: none; }
#side:has(#note:not([hidden])) #rail-note { display: inline-grid; }
@media (max-width: 760px) {
  #btn-side-hide { display: none; }
  :root[data-pop] #scrim { display: block; }
}
#btn-rail-hide { position: absolute; top: 14px; left: 8px; z-index: 1; }
.icon .pane { fill: var(--fg-2); }
.icon:hover .pane { fill: var(--fg); }
/* ---------- the window's frame ----------
   In the native window the page is the frame, and ui/frame.js draws the
   bar's three buttons and carries their rules: a tab never has a frame to
   draw. Only the grip's rest state is here, so the rail's top is nothing
   until that chunk says it is somewhere to drag. */
.rail-grip { display: none; }
/* Pin each pane to its named area. Without this a hidden sidebar leaves the grid
   and the document pane slides into the zero-width first column. */
#side { grid-area: side; }
#main { grid-area: main; }
#rail { grid-area: rail; }
#main { min-width: 0; overflow-y: auto; overscroll-behavior: contain; scroll-behavior: smooth; }
/* Parts the OS would draw, drawn once, snyvi's way (docs/DESIGN.md §8.2).
   Every scrolling list in the chrome has the same thin bar -- there when a
   list is longer than its place, and no wider than it needs to be -- and the
   document keeps the browser's. A placeholder is faint ink; a row dragged
   is not a link picture; a native control left anywhere wears the accent. */
#trees, #toc, #meta, #pop, #ctx, #palette-list, .help-body { scrollbar-width: thin; scrollbar-color: var(--rule-2) transparent; }
/* On Windows, Chromium (WebView2, Edge) answers `scrollbar-width` and
   `scrollbar-color` with the classic control: a track, and an arrow button
   at each end. It draws the bar below only where both are `auto`, so on
   Windows they are put back to `auto` everywhere -- over the ids above and
   the panel's own, hence the !important -- and every bar, the panels', the
   lists' and the document's, is a slim rounded thumb on no track. */
[data-os="win"] * { scrollbar-width: auto !important; scrollbar-color: auto !important; }
[data-os="win"] ::-webkit-scrollbar { width: 10px; height: 10px; }
[data-os="win"] ::-webkit-scrollbar-track, [data-os="win"] ::-webkit-scrollbar-corner { background: transparent; }
[data-os="win"] ::-webkit-scrollbar-button { display: none; }
[data-os="win"] ::-webkit-scrollbar-thumb { background: var(--rule-2); border: 2px solid transparent; border-radius: 5px; background-clip: padding-box; }
[data-os="win"] ::-webkit-scrollbar-thumb:hover { background-color: var(--fg-3); }
::placeholder { color: var(--fg-3); opacity: 1; }
:is(#side, #rail, #pop) a { -webkit-user-drag: none; }
:is(#side, #pop) summary { list-style: none; }
:is(#side, #pop) summary::-webkit-details-marker { display: none; }
/* Words for a screen reader where the eye has a colour: a running dot, a
   waiting mark (docs/DESIGN.md §5.2, colour is never the only signal). */
/* A drawn glyph in a button: an icon the size of the text it stands in for. */
.g-ico { display: block; flex: none; }
#history .g-ico { display: inline-block; vertical-align: -1px; color: var(--accent); }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { #main, #toc { scroll-behavior: auto; } *, *::before, *::after { transition: none !important; animation: none !important; } }

/* ---------- sidebar ---------- */
.side-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 12px 10px 16px; }
.brand { font-weight: 600; letter-spacing: -.01em; color: var(--fg); display: inline-flex; align-items: center; gap: 8px; }
.brand:hover { text-decoration: none; }
/* snyvi itself, the face from icons/icon-small.svg, and the one mascot on
   screen: the note at the foot of the sidebar is its voice. Calm and awake
   most of the time; a waiting note perks it up (the nub pulses, it blinks,
   it hops once when the note arrives); a reader resting on the note gets a
   smile and a heart. It falls asleep while the page has no daemon to hear --
   it stopped, or an upgrade replaced it. */
.brand-mark { width: 20px; height: 20px; display: inline-block; flex: none; }
.brand-mark svg { display: block; width: 100%; height: 100%; overflow: visible; }
.brand-mark .bm-body { fill: var(--mascot); transition: opacity var(--t); }
.brand-mark .bm-nub { fill: var(--mascot-nub); transform-origin: 16px 5px; transform-box: view-box; transition: transform var(--t), opacity .2s ease; }
.brand-mark .bm-heart { fill: var(--heart); opacity: 0; transform: scale(.4); transform-origin: 16px 3px; transform-box: view-box; transition: opacity .2s ease, transform .3s cubic-bezier(.3,1.6,.5,1); }
.brand-mark .bm-open { transform-box: fill-box; transform-origin: center; }
.brand-mark .bm-open { fill: var(--mascot-ink); }
.brand-mark .bm-shut, .brand-mark .bm-glad, .brand-mark .bm-mouth { fill: none; stroke: var(--mascot-ink); stroke-width: 2.2; stroke-linecap: round; }
.brand-mark .bm-shut, .brand-mark .bm-glad { display: none; }
/* A few blinks and pulses when the note comes, then the nub rests lit. Not
   for as long as the note waits: that can be hours, and an endless animation
   of a fill repaints the page on every display frame for all of them. */
html[data-note] .brand-mark .bm-open { animation: bm-blink 4.6s 2; }
html[data-note="lit"] .brand-mark .bm-nub { fill: color-mix(in srgb, var(--brand) 60%, #fff); animation: bm-ping 1.6s ease-in-out 4; }
.brand-mark.hop svg { animation: bm-hop .9s cubic-bezier(.3,1.6,.5,1) 1; transform-origin: 50% 100%; }
html[data-note-near] .brand-mark .bm-open { display: none; }
html[data-note-near] .brand-mark .bm-glad { display: inline; }
html[data-note-near] .brand-mark .bm-nub { opacity: 0; }
html[data-note-near] .brand-mark .bm-heart { opacity: 1; transform: scale(1); }
@keyframes bm-blink { 0%, 92%, 100% { transform: scaleY(1); } 95% { transform: scaleY(.1); } }
@keyframes bm-ping { 50% { fill: var(--mascot-nub); } }
@keyframes bm-hop { 0% { transform: none; } 35% { transform: translateY(-5px) scale(1.06, .94); } 60% { transform: translateY(0) scale(.94, 1.06); } 80% { transform: translateY(-1.5px); } 100% { transform: none; } }
html[data-link="off"] .brand-mark .bm-body { opacity: .55; }
html[data-link="off"] .brand-mark .bm-nub { transform: translateY(1.5px) scaleY(.7); }
html[data-link="off"] .brand-mark .bm-open, html[data-link="off"] .brand-mark .bm-glad { display: none; }
html[data-link="off"] .brand-mark .bm-shut { display: inline; }

/* ---------- snyvi answers ---------- */
/* The note at the foot of the sidebar is what an agent said, and it carries a
   byline to say who. This is snyvi speaking for itself, so it has no byline
   and no card: a short line in a bubble under the face, and a face to match.
   It never opens on its own -- only for a reader who comes over and rests
   there -- and never over a note still waiting to be read.
   These rules follow the ones above on purpose: `html[data-say]` and
   `html[data-note]` weigh the same, so the later block is the one that wins
   while snyvi is talking, and the waiting-note blink stops for the moment. */
.brand-mark .bm-love, .brand-mark .bm-wink { display: none; }
.brand-mark .bm-love path { fill: var(--heart); }
.brand-mark .bm-wink { fill: none; stroke: var(--mascot-ink); stroke-width: 2.2; stroke-linecap: round; }
html[data-say] .brand-mark .bm-open { animation: none; }
html[data-say] .brand-mark svg { animation: bm-nod .5s ease 1; transform-origin: 50% 100%; }
html[data-say="glad"] .brand-mark .bm-open { display: none; }
html[data-say="glad"] .brand-mark .bm-glad { display: inline; }
html[data-say="wink"] .brand-mark .bm-l { display: none; }
html[data-say="wink"] .brand-mark .bm-wink { display: inline; }
html[data-say="love"] .brand-mark .bm-open { display: none; }
html[data-say="love"] .brand-mark .bm-love { display: inline; }
html[data-say="love"] .brand-mark .bm-heart { opacity: 1; transform: scale(1); }
@keyframes bm-nod { 0%, 100% { transform: none; } 40% { transform: translateY(-2px) scale(1.04, .97); } }
/* It says it where its own name is written. A bubble under the face was a
   card hanging over the sidebar, covering the first rows of the tree to say
   two words; this covers nothing, moves nothing, and costs no space at all --
   the word "snyvi" steps aside for the length of the answer and comes back.
   The logo speaking in place of its own name is also the right shape for the
   thing it is: snyvi only ever answers, so it only ever borrows its own line.
   Short lines only, then: it has the width between the mark and the count. */
.brand { position: relative; }
.brand-name { transition: opacity var(--dur-quick) ease; }
html[data-say] .brand-name { opacity: 0; }
/* How many agents hold a stream on the daemon now, and the way to the connect
   page. It was a row of its own, "Agents live", which spent a line of the
   sidebar on a number read rarely; it is a pill at the foot, beside the
   version, the dot saying what the number counts and the tooltip naming who.
   The id and the tooltip stay: bench/ui.mjs reads both. */
.side-head .brand { margin-right: auto; }
.live { display: inline-flex; align-items: center; gap: 5px; margin-left: auto; padding: 1px 7px; border-radius: var(--r-pill); background: var(--rule); color: var(--fg-3); font-size: var(--fs-micro); font-weight: 600; line-height: 16px; transition: background var(--t), color var(--t); }
.live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .6; }
.live:hover { color: var(--fg); text-decoration: none; }
.live.on { color: var(--fg-2); }
.live.on::before { opacity: 1; background: var(--ok); box-shadow: var(--led-glow); }
/* None connected: a ring rather than a dot, the same size, so "0" does not
   read as a light that is on. */
.live:not(.on)::before { background: none; box-shadow: inset 0 0 0 1.5px currentColor; }
/* The update pill's look is about.js's, which draws it (1.7.2). */
/* The page cannot hear the daemon; a stale count is worse than none. */
html[data-link="off"] .live { visibility: hidden; }
.icon { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: var(--r-sm); color: var(--fg-3); transition: background var(--t), color var(--t); }
.icon:hover { background: var(--rule); color: var(--fg); }
.icon.on { color: var(--accent); background: var(--accent-bg); }
/* A control that means nothing in this view: faded, still focusable, and
   its label says why (app.js, `why`). An "on" one fades with it -- wrap left
   on reads as off on a desk, where it does nothing. */
.icon.dim, .icon.dim:hover { color: color-mix(in srgb, var(--fg-3) 45%, transparent); background: none; cursor: default; }
/* Drawn in the face it has picked, so the button says which one it is. */
.icon .aa { font-size: var(--fs-ui); font-weight: 600; letter-spacing: -.02em; font-family: var(--body); }
:root[data-font="mono"] .icon .aa { font-size: var(--fs-micro); letter-spacing: -.06em; }

.side-foot { display: flex; align-items: center; gap: 4px; padding: 8px 12px 10px; margin-top: auto; border-top: 1px solid var(--rule); }
.side-foot { position: relative; }
/* The version, quiet at the foot's end: a click is About, and an update
   waiting puts its dot on it. Gone when the sidebar is folded; the rule stays. */
.foot-ver { position: relative; padding: 2px 6px; border-radius: 4px; font-family: var(--mono); font-size: 11px; color: var(--fg-3); font-variant-numeric: tabular-nums; transition: background var(--t), color var(--t); }
.foot-ver:hover { background: var(--rule); color: var(--fg-2); }
.foot-ver.upd::after { content: ""; position: absolute; top: 1px; right: 0; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--bg-side); }
:root[data-side="0"] .foot-ver { display: none; }
/* The accent: the swatch on the button is the colour in use, and a click is
   the next one. It was a popover of eight, which is a menu to read for a
   setting whose only real answer is what the page looks like in it; the
   window changing under the click says more than the swatches did. The flick
   is so the button does not sit perfectly still while everything around it
   changes colour. */
.icon .sw { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent); transition: background var(--t); }
.icon.flick .sw { animation: sw-flick var(--dur-move) var(--ease-out) 1; }
@keyframes sw-flick { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }
/* The footer's last line held five icons and a hint, and every one of them was
   a control a reader sets once and never touches again -- the accent, the
   reading face, the width, the wrap, the keys -- each spending a permanent
   slot on itself. Only the light/dark switch is left standing, because it is
   the one a reader reaches for daily, and the rest grow up out of it in a
   column when the pointer rests there. The switch is both the anchor and the
   whole affordance: hovering it is how you find out the others are there.
   The column is a descendant of the switch's box, so reaching up into it
   never leaves the hover -- that is what `.foot-set` is for -- and its bottom
   edge touches that box, so there is no gap of dead ground to cross either.
   Deliberately not `visibility: hidden`: the column has to stay focusable so
   Tab reaches it, and reaching it is what opens it for a keyboard.
   The switch is the column's bottom step and not a button standing under it:
   the outline goes around the whole column, switch included, so what opens
   reads as one thing growing out of what was already there rather than a
   second thing arriving above it. So the column holds the switch at all
   times and only ever draws its border -- nothing at rest, and the switch
   sits in it exactly where it sat in the bare row. Open, it is filled and
   lifted: it was a hairline and no fill, and the sidebar showed through --
   the note's text ran behind the swatch. At rest it is still nothing at all.
   `.foot-set` is the resting footprint, one button's worth; the column is out
   of flow above it, so the rest of the controls cost the footer no height.
   The -6px is the column's own 1px border and 5px of padding, which puts the
   switch back over that footprint to the pixel. */
.foot-set { position: relative; width: 28px; height: 28px; }
.foot-rail { position: absolute; left: -6px; bottom: -6px; z-index: var(--z-pop);
  display: flex; flex-direction: column-reverse; align-items: center; gap: 2px; padding: 5px;
  border-radius: var(--r-md); border: 1px solid transparent; background: transparent;
  /* Closed, the column is still there, invisible and the height of all of it.
     It must not take the clicks meant for the note it hangs over -- so it
     passes them through until it opens, and the switch takes its own. */
  pointer-events: none;
  transition: border-color var(--dur-move) ease, background-color var(--dur-move) ease, box-shadow var(--dur-move) ease; }
.foot-rail .keep { pointer-events: auto; }
.foot-set:hover .foot-rail, .foot-set:focus-within .foot-rail, .foot-rail:has(> button.said) {
  border-color: var(--rule); background: var(--bg-raise); box-shadow: var(--shadow); pointer-events: auto; }
/* Each control arrives a beat after the one below it, so the column reads as
   one thing unfolding rather than five appearing at once. The switch is not
   among them: it was already there. It is held out by a class and not by its
   id, because `:not(#btn-theme)` carries an id's weight -- enough to outrank
   the rule below that fades the others in, which left the column opening on
   nothing at all. */
.foot-rail > button:not(.keep) { opacity: 0; transform: translateY(4px); pointer-events: none;
  transition: opacity var(--dur-quick) ease, transform var(--dur-move) cubic-bezier(.3,1.5,.5,1); }
.foot-set:hover .foot-rail > button, .foot-set:focus-within .foot-rail > button { opacity: 1; transform: none; pointer-events: auto; }
/* The beat each one arrives on, its label beside it, and the column held
   open while it is answered are look.js's, which wires these buttons. */
/* Nothing hovers on a touch screen, so there the row stays as it was. */
@media (hover: none) {
  .foot-set { width: auto; height: auto; }
  /* :is() for the weight of the open column's rules, which would otherwise
     fill the row the moment a finger focuses a button in it. */
  .foot-set:is(*, :hover) .foot-rail { position: static; flex-direction: row; padding: 0; gap: 4px;
    background: none; border: 0; box-shadow: none; pointer-events: auto; }
  .foot-rail > button { opacity: 1; transform: none; pointer-events: auto; }
}
.icon .qm { font-family: var(--sans); font-size: var(--fs-body-s); font-weight: 600; }

#trees { overflow-y: auto; padding: 0 8px 8px; font-size: var(--fs-ui); flex: 1; min-height: 0; }
#tree ul { list-style: none; margin: 0; padding: 0; }
/* Sections, as a Mac sidebar draws them. A head is a quiet label that folds
   what is under it -- the same for Inbox, Desks and Folders -- with its
   chevron shown on hover, or while folded, and its + on the right. Whatever
   opens a page is a row under it, and every row is one shape: 28px tall, a
   16px icon at the section label's edge, the name at 32px, and -- on a row
   that folds -- the heads' chevron right after the name. */
.s-head { display: flex; align-items: center; height: 26px; margin-top: 18px; padding-right: 2px; }
#inbox-row .s-head { margin-top: 4px; }
/* A folded section is only its head, so the next head sits close under it. */
#trees.fold-inbox #desk-nav .s-head, #trees.fold-desks #browse-nav .s-head { margin-top: 2px; }
/* The heads stay while their section scrolls under them. Desks and Folders
   hold their own rows, so each head sticks inside its section and is pushed
   off by the next; each section is opaque and above the one before, so it
   slides over the Inbox's head rather than under it. The Inbox's projects
   are a sibling of its head (the rail's popovers move them apart), so the
   Inbox's head and All documents stick as one, at #trees' top, until Desks
   covers them. flow-root keeps each head's margin inside its section, so
   the section's background covers the gap too. Only at home in #trees: in a
   rail popover each section is on its own. */
/* Their layers are the column's own business: nothing they stack over
   leaves #trees, so nothing outside it is covered. Not on the rail, where
   #pop has to stand over the page. */
:root:not([data-side="0"]) #trees { isolation: isolate; }
#trees > :is(#inbox-row, #desk-nav, #browse-nav) { display: flow-root; background: var(--bg-side); }
#trees > #inbox-row { position: sticky; top: 0; z-index: 1; }
#trees > :is(#desk-nav, #browse-nav) { position: relative; z-index: 2; }
#trees > :is(#desk-nav, #browse-nav) > .s-head { position: sticky; top: 0; z-index: 1; background: var(--bg-side); }
.s-link { flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px; padding: 0 8px; font: inherit; font-size: var(--fs-micro); font-weight: 600; letter-spacing: .01em; color: var(--fg-3); background: none; border-radius: var(--r-sm); cursor: pointer; text-align: left; transition: color var(--t); }
.s-link:hover { color: var(--fg-2); }
.s-nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-chev { width: 5px; height: 5px; margin-left: 4px; border: solid currentColor; border-width: 0 1.5px 1.5px 0; transform: translateY(-1.5px) rotate(45deg); opacity: 0; transition: transform var(--t), opacity var(--t); }
.s-head:hover .s-chev, .s-link:focus-visible .s-chev, .s-link[aria-expanded="false"] .s-chev { opacity: 1; }
.s-link[aria-expanded="false"] .s-chev { transform: rotate(-45deg); }
.s-add { flex: none; display: grid; place-items: center; width: 22px; height: 22px; color: var(--fg-3); font-size: 16px; font-weight: 400; line-height: 1; border-radius: var(--r-sm); opacity: 0; transition: opacity var(--t), background var(--t), color var(--t); }
.s-head:is(:hover, :focus-within) .s-add, #browse-nav:has(.b-empty) .s-add { opacity: 1; }
.s-add:hover { background: var(--rule); color: var(--fg); }
.s-blk { flex: none; margin-right: 4px; color: var(--warn); font-size: var(--fs-micro); font-weight: 600; }
:root:not([data-side="0"]) :is(#trees.fold-inbox :is(#inbox-row .s-row, #queue, #tree), #trees.fold-desks .t-desks, #trees.fold-folders .b-body) { display: none; }
.s-body { list-style: none; margin: 0; padding: 0; }
.s-empty { padding: 5px 8px 5px 8px; font-size: var(--fs-small); color: var(--fg-3); }

/* The row. */
.s-row, .t-proj > summary, .b-root > summary, .t-desk > a, .t-back { display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 8px; border-radius: var(--r-sm); color: var(--fg); font-weight: var(--fw-row); white-space: nowrap; cursor: pointer; list-style: none; position: relative; transition: background var(--t), color var(--t); }
.s-row:hover, .t-proj > summary:hover, .b-root > summary:hover, .t-desk > a:hover { background: var(--rule); color: var(--fg); text-decoration: none; }
/* The current row: a tint and a 2 px bar in the accent, the text staying
   ink. Solid accent text on a tint read as an alert, not as "you are here". */
.s-row.active, .t-desk > a.active { background: var(--accent-bg); color: var(--fg); box-shadow: inset 2px 0 0 var(--accent); }
/* A folded project that holds the document on screen: the bar and the icon,
   no tint, a quieter "in here". Colour only, so nothing moves. */
.t-proj > summary.holds-current { box-shadow: inset 2px 0 0 var(--accent); }
.t-proj > summary.holds-current > .r-ico { color: var(--accent); }
.r-ico { flex: none; color: var(--fg-3); transition: color var(--t); }
.s-row.active .r-ico, .t-desk > a.active .r-ico { color: var(--accent); }
.s-row .title, .t-desks .title { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.s-row .n { margin-left: auto; font-size: var(--fs-micro); color: var(--fg-3); font-variant-numeric: tabular-nums; }
/* A row that holds something folds it with the section heads' chevron,
   right after its name: shown on hover and on focus, and kept while the row
   is folded, pointing at the name, so a folded row says so at rest. */
summary > .s-chev { flex: none; margin-left: -2px; color: var(--fg-3); }
summary:is(:hover, :focus-visible) > .s-chev, details:not([open]) > summary > .s-chev { opacity: 1; }
details:not([open]) > summary > .s-chev { transform: rotate(-45deg); }
/* The ladder: each level's icons hang under its parent's name, 32px in,
   as .b-tree next door compounds it. It was once six a level -- and the
   font grew with depth, so the deepest rows were the largest and the least
   indented at once. */
.t-wf { margin: 2px 0 6px 24px; }
.t-wf > .wf-name { font-size: var(--fs-micro); font-weight: 600; color: var(--fg-3); padding: 4px 8px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.t-doc a { display: flex; align-items: baseline; gap: 6px; padding: 4px 8px; border-radius: var(--r-sm); color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; position: relative; transition: background var(--t), color var(--t); }
.t-doc a:hover { background: var(--rule); color: var(--fg); text-decoration: none; }
.t-doc a.active { background: var(--accent-bg); color: var(--fg); box-shadow: inset 2px 0 0 var(--accent); }
/* No mark in front of a row: its icon is the mark. A read row's glyph is
   quiet, and centred on the line rather than sat on its baseline; a row
   waiting to be read has it in the accent, with its title in ink -- the
   same in the queue and in the tree, gone once the document is opened from
   either -- and the open row is lit whole. */
:is(.t-doc a, .b-file a) > .r-ico { align-self: center; opacity: .75; }
:is(.t-doc a.new, .t-doc a.active, .b-file a.active) > .r-ico { color: var(--accent); opacity: 1; }
.t-doc a.new .title { color: var(--fg); font-weight: 600; }
.t-queue { margin: 0 0 6px; }
.t-queue .t-label { display: flex; align-items: center; padding-top: 2px; }
/* The count as a figure in the accent, not a filled pill: the bar over the
   page is the one that interrupts, and this only says how many. */
.t-queue .t-label .n { margin-left: auto; color: var(--accent); font-weight: 600; padding: 0 6px; line-height: 16px; letter-spacing: 0; font-size: var(--fs-micro); font-variant-numeric: tabular-nums; }
.t-queue ul { list-style: none; margin: 2px 0 0; padding: 0; }
.t-queue .t-more { margin-left: 8px; }
.t-queue .t-more a { display: inline-block; font-size: var(--fs-micro); color: var(--fg-2); padding: 3px 6px; border-radius: var(--r-sm); }
.t-queue .t-more a:hover { background: var(--rule); color: var(--fg-2); text-decoration: none; }
.t-doc .title { overflow: hidden; text-overflow: ellipsis; }
/* What moved in the sidebar, said once. An arrival's row, or one an undo put
   back, is lit the way a heading is where a jump landed -- the same wash, so
   the eye learns one sign. A row that was read or deleted closes where it
   was, 140 ms, before the list moves up into its place. Both start at a
   negative delay when the tree is rebuilt under them (see `moment` in
   app.js), so a rebuild never plays them again. */
.wash { animation: land 700ms ease-out; border-radius: var(--r-sm); }   /* on the row itself, where its moment is; /start's Show me too */
@media (prefers-reduced-motion: reduce) { .wash, .prose .flash { outline: 2px solid var(--focus); outline-offset: 1px; } }
.t-doc.leaving, .t-queue.leaving .t-label { animation: leave var(--dur-quick) var(--ease-out) forwards; overflow: hidden; pointer-events: none; }
/* A row leaving fades where it stands; its place goes when it does. Never
   an animated height: that is layout, every frame (docs/DESIGN.md §7.2). */
@keyframes leave { to { opacity: 0; } }
/* A removed document's row, standing where it was on the row's own grid, so
   nothing below it moves: the title dims, says "removed", and holds the Undo.
   The bar along its foot is the time the offer has left: app.js's clock
   (undoClock) writes it into --undo-left, and stops while a pointer or a
   focus rests on the row. The bar only moves in a short transition between
   the clock's ticks, so under reduced motion it is still and still says
   what is left. `--t` starts the row's fade where the last draw left it, so
   a redraw never plays it again. */
.t-ghost { display: flex; align-items: baseline; gap: 6px; padding: 4px 8px; border-radius: var(--r-sm); color: var(--fg-3); white-space: nowrap; overflow: hidden; position: relative; }
.t-gone:not(.leaving) > .t-ghost { animation: ghost-in var(--dur-quick) ease-out both; animation-delay: var(--t, 0ms); }
@keyframes ghost-in { from { color: var(--fg-2); } }
.t-ghost > .r-ico { align-self: center; opacity: .4; }
.t-ghost > .title { min-width: 0; }
.t-gone > .t-ghost > .k { margin-left: 0; font-family: inherit; font-size: var(--fs-micro); }
.t-ghost > .t-undo { margin-left: auto; align-self: center; padding: 2px 7px; }
:is(.t-ghost, .t-back)::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: 1px; height: 2px; border-radius: 1px; background: var(--accent); opacity: .6; transform-origin: left; transform: scaleX(var(--undo-left, 1)); transition: transform var(--dur-instant) linear; }
.t-gone.leaving { animation-delay: var(--t, 0ms); }
/* What stands around a ghost only for its sake -- the same document's row in
   Waiting, a project that left with its last document -- is there to be read
   past, not used, and closes when the ghost does. */
.t-doc.held > a { opacity: .45; pointer-events: none; }
.t-proj.leaving > summary { animation: leave var(--dur-quick) var(--ease-out) forwards; overflow: hidden; pointer-events: none; }
.t-doc .k { margin-left: auto; font-size: var(--fs-micro); color: var(--fg-2); font-family: var(--mono); flex: none; font-variant-numeric: tabular-nums; }
.t-doc .pin { color: var(--fg-2); flex: none; display: inline-flex; align-self: center; }
/* Both names in the tree are guesses, so each carries the means to correct it,
   kept out of the way until the row is under the cursor. The name takes the free
   space so the badge and the control sit at the edge. */
.t-wf > .wf-name > .nm, .t-back > .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A project's name and a folder's give way to their chevron, so they size
   to themselves, and the first control after it takes the free space. */
:is(.t-proj, .b-root) > summary > .nm, .b-dir > details > summary > .nm { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t-proj > summary > .ren { margin-left: auto; }
.t-wf > .wf-name { display: flex; align-items: baseline; gap: 4px; }
.ren { color: var(--fg-3); font-size: var(--fs-micro); line-height: 1; padding: 2px 4px; border-radius: var(--r-xs); opacity: 0; flex: none; letter-spacing: 0; }
.t-proj > summary:hover .ren, .t-wf > .wf-name:hover .ren, .t-desk > a:hover .ren, .ren:focus-visible { opacity: 1; }
.ren:hover { background: var(--rule-2); color: var(--fg); }
/* The field shows the name as stored, not as the summary styles it. */
.ren-in { flex: 1; min-width: 0; font: inherit; text-transform: none; letter-spacing: normal; color: var(--fg); background: var(--bg); border: 1px solid var(--accent); border-radius: var(--r-xs); padding: 1px 4px; }
.ren-in:focus { outline: none; }
/* Why a field is open again (a rename, a note): under it, until the next key. */
.field-err { position: absolute; left: 8px; top: 100%; z-index: 5; padding: 2px 6px; border-radius: var(--r-xs); background: var(--bg-raise); box-shadow: var(--shadow); color: var(--danger); font-size: var(--fs-micro); white-space: nowrap; }
:has(> .field-err) { position: relative; }
/* A list the daemon did not send, in its own place, with its Retry. */
.no-reach { display: flex; align-items: baseline; gap: 8px; padding: 4px 8px; color: var(--fg-2); font-size: var(--fs-small); list-style: none; }
.no-reach button { color: var(--accent); font: inherit; font-weight: 500; }
/* Past a cap, and while a project's rows are on their way. Both sit where a
   document row would, and neither should read as one: what is behind a row the
   tree has not drawn is an offer, not a document. */
.t-more { margin: 1px 0 2px 14px; }
.t-more button { font: inherit; font-size: var(--fs-micro); color: var(--fg-2); background: none; border: 0; padding: 3px 6px; border-radius: var(--r-sm); cursor: pointer; }
.t-more button:hover { background: var(--rule); color: var(--fg-2); }
.t-more button:disabled { cursor: default; opacity: .5; }
.t-wait { padding: 4px 8px 4px 14px; font-size: var(--fs-small); color: var(--fg-3); }
.t-wait .sk-bar { height: 8px; margin: 6px 0; }
.t-empty { padding: 24px 12px; color: var(--fg-3); font-size: var(--fs-ui); line-height: 1.6; }
.t-empty code { font-family: var(--mono); font-size: var(--fs-small); background: var(--rule); padding: 1px 5px; border-radius: var(--r-xs); }

/* desks: the row, the flat list under it, the + on a folder, and the folder
   menu -- the only ways a desk is reached. The desk view itself is styled by
   ui/desk.js, which is not on the wire until a desk is opened. */
/* A desk's row ends in one column, drawn on every row: a 10px box for the
   mark -- a green dot running, an amber ! waiting, nothing idle -- and the
   count, right-aligned. Both were laid out only when there was something to
   show, so the dot on one row sat where the count did on the next. */
/* A desk row on its way to another place: the line is where it lands, and
 * nothing else moves until it does. */
.t-drop { position: fixed; height: 2px; border-radius: 1px; background: var(--accent); pointer-events: none; z-index: var(--z-pop); }
.t-desk > a.dragging { opacity: .55; }
.desk-drag, .desk-drag * { cursor: grabbing !important; user-select: none; }
.t-desks .t-said { color: var(--warn); font-size: var(--fs-micro); font-weight: 600; white-space: nowrap; }
.t-desks .end { display: flex; align-items: center; gap: 4px; flex: none; margin-left: auto; }
.t-desks .ctx { font-size: var(--fs-micro); color: var(--fg-3); font-variant-numeric: tabular-nums; }
.t-desks .ctx.hot { color: var(--warn); font-weight: 600; }
.t-desks .dot { flex: none; display: grid; place-items: center; width: 10px; height: 10px; }
.t-desks .dot.on::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }
.t-desks .dot.blk { width: auto; color: var(--warn); font-size: var(--fs-micro); font-weight: 600; line-height: 1; white-space: nowrap; }
.t-desks .k { min-width: 2ch; text-align: right; font-size: var(--fs-micro); font-family: var(--mono); color: var(--fg-2); flex: none; font-variant-numeric: tabular-nums; }
/* A row's ✕ -- delete a document, close a desk -- takes the place of what is
   at the row's end on hover. Armed, a desk's says Close? */
/* Hidden by clipping rather than by `display: none`, which is what it was:
   an element that is not displayed cannot be focused, so `.row-x:focus-visible`
   never matched and Tab walked straight past the ✕ on every row. It keeps no
   space at rest -- zero width, and the row's own end still shows through --
   and it is in the focus order, where a keyboard reader can reach it. */
/* No auto margin at rest: the row's end column already carries one, and
   two auto margins in a row split the slack between them, which put the
   dot and the count halfway along the row and a different halfway on each. */
.row-x { flex: none; margin-left: 0; width: 0; padding: 0; overflow: hidden; font: inherit; font-size: var(--fs-micro); line-height: 1; color: var(--fg-3); background: none; border-radius: var(--r-xs); cursor: pointer; }
/* A project row's tools each own a 22 px slot at the row's end, there at
   rest and on hover alike: showing one is its opacity, never its width, so
   the desk glyph beside the ✕ does not jump left when the pointer arrives. */
.t-proj > summary > :is(.b-new, .row-x) { flex: none; width: 22px; height: 22px; padding: 0; display: grid; place-items: center; overflow: visible; }
.t-proj > summary > .row-x { margin-left: auto; opacity: 0; }
.t-proj > summary:is(:hover, :focus-within) > .row-x, .row-x:focus-visible, .arming .row-x { opacity: 1; }
/* On a desk's row and a document's, the tools stand over the row's end
   rather than in the row: coming out, they move nothing, and the title's
   ellipsis stays where it was. The end they cover keeps its place, unseen.
   Faded rather than removed, so Tab still reaches them. Their ground is the
   row's colour laid over the sidebar's -- the dark theme's is translucent,
   and on its own would let the title show through. */
:is(.t-doc, .t-desk) > a { --row: var(--rule); }
:is(.t-doc, .t-desk) > a.active { --row: var(--accent-bg); }
:is(.t-doc, .t-desk) > a > :is(.ren, .row-x) { position: absolute; top: 50%; translate: 0 -50%; right: 6px; width: auto; margin: 0; padding: 2px 4px; opacity: 0; pointer-events: none; background: linear-gradient(var(--row) 0 0), var(--bg-side); }
.t-desk > a > .ren { right: 26px; }
:is(.t-doc, .t-desk) > a:is(:hover, :focus-visible, :has(:focus-visible)) > :is(.ren, .row-x), .arming > a > .row-x { opacity: 1; pointer-events: auto; }
:is(.t-doc, .t-desk) > a > :is(.ren, .row-x):hover { background: linear-gradient(var(--rule-2) 0 0), linear-gradient(var(--row) 0 0), var(--bg-side); }
.t-doc > a:is(:hover, :has(:focus-visible)) :is(.k, .pin), .arming > a :is(.end, .k), .t-desk > a:has(> [data-armed]) > .ren { visibility: hidden; }
/* A desk's ✕ has a slot of its own at the row's end, as a project's does:
   hovering shows it and hides nothing, so the dot and the count beside it
   stay where they are. Armed, "Close?" lays over the end as before. */
.t-desk > a > .row-x { position: static; translate: none; flex: none; width: 22px; height: 22px; padding: 0; margin: 0 -4px 0 0; display: grid; place-items: center; background: none; }
.t-desk > a > .row-x[data-armed] { position: absolute; right: 6px; width: auto; height: auto; padding: 2px 4px; background: linear-gradient(var(--row) 0 0), var(--bg-side); }
/* A row lights the moment the pointer is on it, and only lets go slowly. */
:is(.s-row, .t-proj > summary, .b-root > summary, .t-desk > a, .t-doc a):hover { transition-duration: 0s; }
.row-x:hover { background: var(--rule-2); color: var(--fg); }
/* The way back, under the tree while anything is out of it: quiet, because it
   is a state of the list rather than something to do, and always there, so
   nothing a reader put away can be lost by forgetting it was put away. */
.t-away { display: block; width: 100%; text-align: left; padding: 4px 8px 4px 18px; margin-top: 2px; font-size: var(--fs-micro); color: var(--fg-3); border-radius: var(--r-sm); }
.t-away:hover { background: var(--rule); color: var(--fg-2); }
/* The quiet projects past the Inbox's cap (app.js, `inboxCap`), said in one
   row on the rows' own grid: the icon column, "N more", the chevron that
   folds it, and at the end how long they have been quiet. A step dimmer than
   a project: it is the list's edge, not one of its things. */
.t-quiet { display: flex; align-items: center; gap: 8px; width: 100%; height: 28px; padding: 0 8px; border-radius: var(--r-sm); color: var(--fg-2); font-weight: var(--fw-row); white-space: nowrap; text-align: left; position: relative; transition: background var(--t), color var(--t); }
.t-quiet:hover { background: var(--rule); color: var(--fg); transition-duration: 0s; }
.t-quiet > .nm { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.t-quiet > .s-chev { flex: none; margin-left: -2px; color: var(--fg-3); opacity: 1; }
.t-quiet[aria-expanded="false"] > .s-chev { transform: rotate(-45deg); }
/* "Show less" ends the list it let out, so its chevron points back up it. */
.t-quiet[aria-expanded="true"] > .s-chev { transform: translateY(1.5px) rotate(-135deg); }
.t-quiet > .k { margin-left: auto; font-size: var(--fs-micro); color: var(--fg-3); flex: none; }
/* Something waiting in there, or the document on screen: the mark a folded
   project gets, and "more" stays shut until it is clicked. */
.t-quiet.new > .k, .t-quiet.new > .r-ico, .t-quiet.holds-current > .r-ico { color: var(--accent); }
.t-quiet.holds-current { box-shadow: inset 2px 0 0 var(--accent); }
/* A project nothing has come from in a week steps its name back, and one
   with something waiting lights its icon, as a waiting row does. Colour
   only: the list keeps its order and every row its place. */
.t-proj.quiet > summary > .nm { color: var(--fg-2); }
.t-proj > summary.new > .r-ico { color: var(--accent); }
/* An idle desk's name steps back, so the live ones -- the dot, "needs you"
   -- are what the eye finds. The desk on screen keeps its ink. */
.t-desk > a:not(.active):not(:has(.dot)) > .title { color: var(--fg-2); }
/* A row's fold chevron comes up a step under the pointer, so it reads as
   something to press. */
summary:is(:hover, :focus-visible) > .s-chev { color: var(--fg-2); }
/* The offer to undo, standing in the row's own place and on the row's own
   grid: what the sidebar just did is said in the sidebar, where the click was,
   so the eye does not have to cross the window to a corner to find out. It is
   the row's ghost -- dimmed, no icon, no chevron -- and it holds the list
   still while it stands, so taking the offer moves nothing. */
.t-back { position: relative; color: var(--fg-3); font-size: var(--fs-small); }
.t-undo { flex: none; font-size: var(--fs-micro); line-height: 1; padding: 3px 7px; border-radius: var(--r-xs); color: var(--accent); }
.t-undo:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.row-x[data-armed] { color: var(--danger); font-weight: 600; }
.row-x[data-armed]:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }
:is(.t-doc, .t-desk) > a > .row-x[data-armed]:hover { background: linear-gradient(color-mix(in srgb, var(--danger) 12%, transparent) 0 0), linear-gradient(var(--row) 0 0), var(--bg-side); }
.b-new { margin-left: auto; display: inline-flex; align-items: center; color: var(--fg-3); line-height: 1; padding: 2px 4px; border-radius: var(--r-xs); opacity: 0; flex: none; }
/* The glyph is the one the Desks section uses, so what the button makes is
   named by its shape rather than by a plus that could mean any addition. */
.b-new .r-ico { color: inherit; }
summary:is(:hover, :focus-within) > .b-new, .b-new:focus-visible { opacity: 1; }
.b-new:hover { background: var(--rule-2); color: var(--fg); }
.b-new + .b-close { margin-left: 0; }
/* A project's glyph stays while the project has a desk: that is the row
   saying so, and the way to it. Its ✕ follows it rather than splitting the
   slack with it. */
.b-new.has { opacity: 1; color: var(--fg-3); }
/* …in the Desks rows' colours while its desk is doing something: a panel
   running, or one waiting on the reader. Idle, it stays quiet, so it does
   not read as a second desk row. */
.b-new.has.on { color: var(--ok); }
.b-new.has.blk { color: var(--warn); }
/* A project with no desk yet keeps the slot, empty until the row is hovered,
   and then says "New desk": half-lit at rest read as a fault, not a state. */
.t-proj > summary > .b-new + .row-x { margin-left: 0; }
/* browse: folders read straight from disk */
.t-label { padding: 8px 10px 2px; font-size: var(--fs-micro); color: var(--fg-3); font-weight: 600; }
.b-root > summary:hover { background: var(--rule); color: var(--fg); }
.b-close { margin-left: auto; color: var(--fg-3); font-size: var(--fs-micro); line-height: 1; padding: 2px 4px; border-radius: var(--r-xs); opacity: 0; }
.b-root > summary:is(:hover, :focus-visible) .b-close { opacity: 1; }
.b-close:hover { background: var(--rule-2); color: var(--fg); }
.b-close:focus-visible { opacity: 1; }
/* Each level hangs its icons under its parent's name. */
.b-tree { list-style: none; margin: 0; padding: 0 0 0 20px; }
.b-root > .b-tree { padding-left: 24px; }   /* under a 16px glyph and its 8px gap; deeper, a 14px one and 6 */
.b-tree .b-empty { padding: 3px 10px; color: var(--fg-3); font-size: var(--fs-small); font-style: italic; }
.b-dir > details > summary { list-style: none; cursor: pointer; padding: 3px 8px; border-radius: var(--r-sm); color: var(--fg-2); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.b-dir > details > summary:hover { background: var(--rule); color: var(--fg); }
.b-file a { display: flex; align-items: baseline; gap: 6px; padding: 3px 8px; border-radius: var(--r-sm); color: var(--fg-2); white-space: nowrap; overflow: hidden; transition: background var(--t), color var(--t); }
.b-file a:hover { background: var(--rule); color: var(--fg); text-decoration: none; }
.b-file a.active { background: var(--accent-bg); color: var(--fg); box-shadow: inset 2px 0 0 var(--accent); }
.b-file .title { overflow: hidden; text-overflow: ellipsis; }
.b-file .k { margin-left: auto; font-size: var(--fs-micro); color: var(--fg-2); font-family: var(--mono); flex: none; }
/* A previewed page is somebody else's design; give it the pane and no styling of ours. */
/* One scroll region: the frame fills what is left of the pane, so the page itself
   does not also scroll behind it. */
#main:has(article.preview) { overflow: hidden; }
#doc:has(article.preview) { display: flex; flex-direction: column; height: calc(100% - var(--head-h)); padding-top: 8px; padding-bottom: 24px; }
#doc:has(article.preview) .doc-head { margin-bottom: 20px; flex: none; }
article.preview { margin: 0; padding: 0; flex: 1 1 auto; min-height: 360px; }
article.preview iframe { width: 100%; height: 100%; border: 1px solid var(--rule); border-radius: var(--r-sm); background: #fff; display: block; }

.browse-image, .doc-image { text-align: center; margin: 1.5em 0; }
.browse-image img, .doc-image img { border-radius: var(--r-sm); max-width: 100%; height: auto; }

/* ---------- document ---------- */
#doc { --doc-pad: 48px; max-width: calc(var(--measure) + 96px); margin: 0 auto; padding: 8px var(--doc-pad) 120px; }
/* A new document arrives rather than snaps: one short rise on a navigation.
   Not on a refresh in place -- a file being watched is saved every few seconds,
   and a body that blinked on each save would be worse than one that moved. */
.doc-head { margin-bottom: 40px; }
.doc-title { font-size: var(--fs-h1); line-height: 1.15; letter-spacing: -.022em; font-weight: 650; margin: 0 0 10px; text-wrap: balance; font-family: var(--head, var(--sans)); }
.doc-sub { color: var(--fg-3); font-size: var(--fs-ui); margin: 0; }
.doc-sub .branch { font-family: var(--mono); font-size: var(--fs-small); }

/* ---------- the shape of a document that is still coming ----------
   A click is answered in its own frame: the title the row already knew, and
   bars where the text will be. The bars wait --sk-wait before they are
   anything at all, so an open that lands at once shows none of this and one
   that does not says so before a reader can wonder whether the click landed.
   Nothing here is measured or restored: it is the page saying "heard you". */
:root { --sk-wait: 200ms; }
.sk-bar {
  display: block; height: .72em; margin: .93em 0; border-radius: var(--r-sm);
  background: var(--fg); opacity: .07;
  animation: sk-in .28s ease var(--sk-wait) both, sk-breathe 1.9s ease-in-out calc(var(--sk-wait) + .28s) infinite;
}
.sk-gap { display: block; height: 1.5em; }   /* the air between two paragraphs */
.doc-title.sk { display: flex; align-items: center; height: 39px; }
.doc-title .sk-bar { height: 26px; margin: 0; }
.doc-sub.sk { height: 17px; }
.doc-sub .sk-bar { height: 11px; margin: 0; }
.sk-body { max-width: var(--measure, none); }
/* The bars rest visible; the fade in is an extra, so with no motion at
   all the waiting still shows. */
@keyframes sk-in { from { opacity: 0; } }
@keyframes sk-breathe { 50% { opacity: .13; } }

.prose { font-family: var(--body); font-size: var(--fs-read); line-height: 1.65; }
:root[data-font="serif"] { --fs-read: 18px; }
:root[data-font="mono"] { --fs-read: 15px; }
:root[data-font="mono"] .prose { line-height: 1.7; }
.prose > * { content-visibility: auto; contain-intrinsic-size: auto 60px; }
/* Not the headings: they are few and cheap, and the containment that comes
   with content-visibility clips to the box, which is where the `#` beside a
   heading was -- drawn outside it, and so never drawn at all. */
.prose > h1, .prose > h2, .prose > h3, .prose > h4, .prose > table { content-visibility: visible; }
/* A table too: as a placeholder it counts as a scroller Tab can stop on,
   and the moment it is laid out and fits, it is not one, and the browser
   drops the focus it just gave it onto the body. Laid out from the start,
   it is a stop only when it really scrolls. */
.prose > :first-child { margin-top: 0; }
.prose p { margin: 0 0 1.2em; text-wrap: pretty; }
.prose h1, .prose h2, .prose h3, .prose h4 { font-family: var(--head, var(--sans)); font-weight: 650; line-height: 1.25; letter-spacing: -.015em; text-wrap: balance; margin: 2.2em 0 .6em; position: relative; scroll-margin-top: calc(var(--head-h) + 12px); }
.prose h1 { font-size: 1.9em; }
.prose h2 { font-size: 1.5em; margin-top: 2.4em; }
.prose h3 { font-size: 1.2em; }
.prose h4 { font-size: 1em; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-2); }
.prose h1 + h2, .prose h2 + h3 { margin-top: 1em; }
/* The anchor carries the id a fragment names, so the browser's own fragment
   scroll -- which it repeats on its own schedule while a page loads -- aims at
   it, not at the heading. The same margin as the heading, so the two agree
   rather than fight over where the section starts. */
.prose a.anchor { position: absolute; left: -1.2em; top: 0; color: var(--fg-3); opacity: 0; font-weight: 400; scroll-margin-top: calc(var(--head-h) + 12px); }
.prose a.anchor::before { content: "#"; }
.prose h1:hover a.anchor, .prose h2:hover a.anchor, .prose h3:hover a.anchor, .prose h4:hover a.anchor { opacity: .7; }
.prose a.anchor:hover, .prose a.anchor:focus-visible { opacity: 1; text-decoration: none; }
/* Just clicked: the mark says so itself for a moment, in place of a toast. */
.prose a.anchor[data-said] { opacity: 1; color: var(--accent); }
.prose a.anchor[data-said]::before { content: "✓"; }
/* A link that leaves snyvi says so before it is followed, since following one
   opens a tab or a browser rather than turning the page. Small, dimmer than
   the link, and never on a link wrapped around an image -- the mark would
   land in the middle of the picture. */
.prose a[data-ext]:not(:has(img))::after {
  content: "↗"; font-size: .75em; line-height: 1; vertical-align: .35em;
  margin-left: .12em; opacity: .55; text-decoration: none;
}
.prose a[data-ext]:hover::after { opacity: .85; }
/* Where a jump landed: the heading is lit for a moment, then not. A jump is
   instant -- see jumpTo in app.js for why it cannot be smooth -- and this is
   what tells the eye where it went. */
.prose .flash { animation: land 700ms ease-out; }
@keyframes land { from { background: var(--accent-bg); } to { background: transparent; } }
.prose ul, .prose ol { padding-left: 1.4em; margin: 0 0 1.2em; }
.prose li { margin: .3em 0; }
.prose li > p { margin-bottom: .4em; }
.prose li.task-list-item, .prose li:has(> input[type=checkbox]) { list-style: none; margin-left: -1.4em; padding-left: 1.6em; position: relative; }
.prose input[type=checkbox] { position: absolute; left: 0; top: .45em; appearance: none; width: 14px; height: 14px; border: 1.5px solid var(--rule-2); border-radius: var(--r-xs); background: var(--bg-raise); pointer-events: none; }
.prose input[type=checkbox]:checked { background: var(--accent); border-color: var(--accent); }
.prose input[type=checkbox]:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid var(--on-accent); border-width: 0 2px 2px 0; transform: rotate(45deg); }
.prose li:has(> input:checked) { color: var(--fg-3); text-decoration: line-through; text-decoration-color: var(--rule-2); }
.prose blockquote { margin: 1.4em 0; padding: .1em 0 .1em 1.2em; border-left: 2px solid var(--accent); color: var(--fg-2); }
.prose blockquote p:last-child { margin-bottom: 0; }
.prose hr { border: 0; border-top: 1px solid var(--rule); margin: 3em auto; width: 40%; }
.prose img { max-width: 100%; height: auto; border-radius: var(--r-sm); display: block; margin: 1.5em auto; }
.prose strong { font-weight: 650; }
.prose mark { background: var(--mark); color: inherit; padding: 0 .15em; border-radius: var(--r-xs); }
.prose kbd { font-family: var(--mono); font-size: .8em; border: 1px solid var(--rule-2); border-bottom-width: 2px; border-radius: var(--r-xs); padding: .05em .4em; }
.prose sup a { font-size: .75em; font-weight: 600; }
.prose .footnotes { margin-top: 3em; padding-top: 1em; border-top: 1px solid var(--rule); font-size: .9em; color: var(--fg-2); }
.prose .footnotes ol { padding-left: 1.4em; }
.prose dl dt { font-weight: 600; margin-top: 1em; }
.prose dl dd { margin: .2em 0 0 1.2em; }
.prose details { border: 1px solid var(--rule); border-radius: var(--r-sm); padding: .5em 1em; margin: 1.2em 0; }
.prose summary { cursor: pointer; font-weight: 600; }

/* tables */
.prose table { border-collapse: collapse; width: 100%; margin: 1.6em 0; font-size: .92em; display: block; overflow-x: auto; font-variant-numeric: tabular-nums; }
.prose thead th { text-align: left; font-weight: 600; font-size: .82em; letter-spacing: .04em; text-transform: uppercase; color: var(--fg-2); border-bottom: 1px solid var(--rule-2); padding: .5em .8em .5em 0; }
.prose td { border-bottom: 1px solid var(--rule); padding: .55em .8em .55em 0; vertical-align: top; }
.prose tr:last-child td { border-bottom-color: var(--rule-2); }
.prose td:last-child, .prose th:last-child { padding-right: 0; }
/* A data table is read by scanning columns, so numbers align and the head stays put. */
/* The page is the only vertical scroll; the table keeps horizontal scroll alone. */
.prose table.data { font-family: var(--mono); font-size: .84em; }
.prose table.data thead th { position: sticky; top: 0; background: var(--bg); z-index: 1; white-space: nowrap; box-shadow: 0 1px 0 var(--rule-2); }
.prose table.data td { white-space: nowrap; padding: .28em 1.1em .28em 0; line-height: 1.45; }
/* Prose columns wrap instead of running off the edge. */
.prose table.data td.wrap, .prose table.data th.wrap { white-space: normal; min-width: 28ch; max-width: 70ch; overflow-wrap: anywhere; }
.prose table.data thead th { padding: .4em 1.1em .4em 0; }
.prose table.data td.num { text-align: right; font-variant-numeric: tabular-nums; }
.prose table.data tbody tr:hover td { background: var(--code-bg); }

/* alerts (GitHub style) */
.prose .markdown-alert { margin: 1.4em 0; padding: .8em 1.1em; border-radius: var(--r-sm); border-left: 3px solid var(--fg-3); background: var(--code-bg); }
.prose .markdown-alert p:last-child { margin: 0; }
.prose .markdown-alert-title { font-family: var(--sans); font-weight: 600; font-size: .85em; letter-spacing: .04em; text-transform: uppercase; margin-bottom: .4em; color: var(--fg-2); }
.prose .markdown-alert-note { border-color: var(--info); } .prose .markdown-alert-note .markdown-alert-title { color: var(--info); }
.prose .markdown-alert-tip { border-color: var(--ok); } .prose .markdown-alert-tip .markdown-alert-title { color: var(--ok); }
.prose .markdown-alert-important { border-color: var(--important); } .prose .markdown-alert-important .markdown-alert-title { color: var(--important); }
.prose .markdown-alert-warning { border-color: var(--warn); } .prose .markdown-alert-warning .markdown-alert-title { color: var(--warn); }
.prose .markdown-alert-caution { border-color: var(--danger); } .prose .markdown-alert-caution .markdown-alert-title { color: var(--danger); }

/* inline code */
.prose code { font-family: var(--mono); font-size: .86em; background: var(--code-bg); padding: .12em .38em; border-radius: var(--r-xs); font-feature-settings: "calt" 0, "liga" 0; }
.prose pre code { background: none; padding: 0; font-size: inherit; }

/* code blocks, in prose and standalone */
pre.code { position: relative; font-family: var(--mono); font-size: var(--fs-ui); line-height: 1.6; background: var(--code-bg); border-radius: var(--r-sm); padding: 14px 0; margin: 1.5em 0; overflow-x: auto; tab-size: 4; font-feature-settings: "calt" 0, "liga" 0; }
pre.code code { display: block; min-width: max-content; counter-reset: ln; } /* reset lives inside the content-visibility subtree, or style containment hides it */
pre.code::before { content: attr(data-lang); position: absolute; top: 8px; right: 12px; font-family: var(--sans); font-size: var(--fs-micro); font-weight: 600; color: var(--fg-3); opacity: 0; transition: opacity var(--t); pointer-events: none; }
pre.code:hover::before { opacity: 1; }
pre.code .copy { opacity: 0; transition: opacity var(--t); }
pre.code:hover .copy { opacity: 1; }
pre.code:hover:has(.copy)::before { opacity: 0; }
pre.code .copy:focus-visible { opacity: 1; }
/* No pointer to hover with: everything that waits for one is simply there,
   the way the diagram tools already are. */
@media (hover: none) {
  .ren, .b-close, .b-new, pre.code .copy, pre.code::before { opacity: 1; }
  .t-proj > summary > .row-x { width: auto; padding: 2px 4px; margin-left: auto; }
  pre.code:has(.copy)::before { opacity: 0; }
  .prose a.anchor { opacity: .5; }
}
pre.code .ln { display: inline; }
/* The number gutter, written once: the wrap rules below hang continuation lines past it. */
pre.code { --num-pad: 14px; --num-w: 3.2ch; --num-gap: 1.6ch; --gutter: calc(var(--num-pad) + var(--num-w) + var(--num-gap)); }
pre.code .ln::before { counter-increment: ln; content: counter(ln); display: inline-block; width: var(--num-w); margin-right: var(--num-gap); padding-left: var(--num-pad); box-sizing: content-box; text-align: right; color: var(--fg-3); opacity: .55; user-select: none; -webkit-user-select: none; }
.kind-markdown pre.code .ln::before { display: none; }
.kind-markdown pre.code .ln { padding: 0 16px; }
.kind-markdown pre.code:has(.ln:nth-of-type(12)) .ln::before { display: inline-block; }
.kind-markdown pre.code:has(.ln:nth-of-type(12)) .ln { padding: 0 16px 0 0; }

/* standalone code / text / diff documents */
.kind-code, .kind-text, .kind-diff { font-family: var(--mono); }
.kind-code pre.code, .kind-text pre.code, .kind-diff pre.code { margin: 0; background: transparent; padding: 0; font-size: var(--fs-ui); }
.kind-code pre.code, .kind-text pre.code { --num-pad: 0px; }   /* the pane already insets these */
.diff .ln::before { display: none !important; }
/* Anything that is not prose wants the pane, not the reading measure. */
#doc:has(.kind-code), #doc:has(.kind-text), #doc:has(.kind-diff),
#doc:has(.kind-table), #doc:has(.kind-image), #doc:has(article.preview), #doc:has(.hm) { max-width: none; padding-left: 40px; padding-right: 40px; }
/* Maximise width: prose gives up its measure and uses the window. */
:root[data-wide="1"] #doc { --doc-pad: 64px; max-width: none; }

/* find bar */
/* ---------- the queue bar ---------- */
/* Above the document while something is waiting, in the document's measure so
   it reads as part of the page and not the chrome. Sticky like the find bar,
   so a reader deep in a long page still sees the count. */
#queue-bar[hidden] { display: none; }
#queue-bar { position: sticky; top: var(--head-h); z-index: 5; height: 0; margin: 0 auto; max-width: calc(var(--measure) + 96px); padding: 0 48px; box-sizing: border-box; font-size: var(--fs-ui); }
/* Its height is nothing, so a bar that appears mid-read lays over the page's
   top margin rather than pushing the text down under the reader; the find
   bar makes room for it instead. The 10 px gap is the card's own offset:
   padding on the bar would give it a height and push the page down, and a
   margin on the card would collapse through the bar and do the same. */
#queue-bar:not([hidden]) ~ #find { padding-top: 58px; }
/* On a desk the head is the desk's own row, and the bar lays over its
   middle, between the name and the buttons, as it did before the head. */
:root[data-view="desk"] #queue-bar { top: 0; }
#queue-bar .qb { position: relative; top: 10px; display: flex; align-items: center; gap: 10px; min-width: 0; background: var(--bg-raise); border: 1px solid var(--rule); box-shadow: var(--shadow); border-radius: var(--r-md); padding: 5px 5px 5px 9px; animation: rise var(--dur-quick) var(--ease-out); }
/* snyvi at the front of the bar, holding what came: the toast's head, the
   size of the count's line, leaning in to the text it is holding up. Its
   feeling comes and goes with the count (app.js, `qbFeel`); at rest the
   head is still, and reads as the one that answers a click. */
/* Marked all read: the same card, saying so, with its Undo and the drain
   bar that says how long the Undo stands. */
#queue-bar .qb.ghost { animation: none; }
/* The Undo on the bar's own line, so the card keeps the bar's height. */
#queue-bar .qb.ghost .t-undo { font-size: inherit; line-height: inherit; }
#queue-bar .qb.ghost .qb-next { color: var(--fg-2); }
#queue-bar .qb.ghost::after { content: ""; position: absolute; left: 10px; right: 10px; bottom: 2px; height: 2px; border-radius: 1px; background: var(--accent); opacity: .6; transform-origin: left; transform: scaleX(var(--undo-left, 1)); transition: transform var(--dur-instant) linear; }
#queue-bar .qb-who { width: 22px; height: 22px; flex: none; display: block; margin-right: -2px; transform: rotate(-6deg); }
#queue-bar .qb-n { flex: none; color: var(--accent); font-weight: 600; font-variant-numeric: tabular-nums; }
/* The count changed under a bar that stayed: the new number settles in. */
#queue-bar .qb-n.tick { animation: tick var(--dur-move) var(--ease-out); }
/* ‹ › hug the count they step (#100). With one waiting they keep their room,
   unseen, so a second arriving moves no word in the bar. */
#queue-bar .qb-step { margin: 0 -7px; }
#queue-bar .qb.one .qb-step { visibility: hidden; }
#queue-bar .qb:not(.one) .qb-n { min-width: 4ch; text-align: center; }
@keyframes tick { from { opacity: 0; transform: translateY(-4px); } }
#queue-bar .qb-next { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg-3); }
#queue-bar .qb-next b { font-weight: 600; color: var(--fg); }
#queue-bar button:not(.icon), #queue-bar .qb-all { flex: none; padding: 4px 10px; border-radius: var(--r-sm); color: var(--fg-2); transition: background var(--t), color var(--t); }
#queue-bar button:not(.icon):hover, #queue-bar .qb-all:hover { background: var(--rule); color: var(--fg); text-decoration: none; }
/* One key cap for the whole chrome (docs/DESIGN.md §8): the queue bar, the
   Inbox, the meta pane, the help and How snyvi works. A document's own kbd
   is the document's; a menu's and a tip's are boxless, keys on the right. */
kbd:not(.prose kbd) { display: inline-block; min-width: 18px; padding: 0 4px; font: 500 var(--fs-micro)/16px var(--mono); text-align: center; white-space: nowrap;
  color: var(--fg-2); background: var(--bg-side); border: 1px solid var(--rule-2); border-radius: var(--r-xs); }
:is(#queue-bar, .inbox-sec, #meta .actions) kbd { margin-left: 6px; }
#queue-bar .icon { width: 26px; height: 26px; }
#find[hidden] { display: none; }
#find { position: sticky; top: var(--head-h); z-index: 5; display: flex; align-items: center; gap: 4px; margin: 0 auto; max-width: calc(var(--measure) + 96px); padding: 10px 48px 0; }
#find > * { background: var(--bg-raise); }
#find-input { flex: 1; font: inherit; font-size: var(--fs-body-s); padding: 7px 12px; border: 1px solid var(--rule-2); border-radius: var(--r-sm); color: inherit; outline: none; box-shadow: var(--shadow); }
#find-input:focus { border-color: var(--accent); }
#find-count { font-size: var(--fs-small); color: var(--fg-3); padding: 0 6px; font-variant-numeric: tabular-nums; background: none; }
#find .icon { border: 1px solid var(--rule); }
mark.find { background: var(--mark); color: inherit; border-radius: var(--r-xs); }
mark.find.cur { background: var(--accent); color: var(--on-accent); }

/* A split diff's look is ui/diff.js's, which draws it. */

/* mermaid.
   The server sends the source in a <pre class="mermaid">; the client swaps each
   one for a <figure class="mmd"> before first paint, so the raw `flowchart TD`
   is never shown to the reader. The rule below is what covers the gap on a page
   whose script has not run yet -- and the whole of it if scripting is off, where
   the source is the only thing there is to show. */
pre.mermaid { background: transparent; text-align: center; padding: 8px 0; overflow: visible; font-family: var(--mono); font-size: var(--fs-small); color: var(--fg-3); white-space: pre-wrap; }
pre.mermaid::before { display: none; }


/* history */
#history { margin-top: 20px; }
#history h4 { margin: 0 0 4px; font-size: var(--fs-micro); color: var(--fg-3); font-weight: 600; }
#history a { display: block; padding: 2px 0; color: var(--fg-2); font-variant-numeric: tabular-nums; }
#history a.cur { color: var(--accent); }
#history a:hover { color: var(--accent); text-decoration: none; }

/* diff */
.diff .ln { display: inline-block; min-width: 100%; padding: 0 12px; }
.diff .add { background: var(--add); color: var(--add-fg); }
.diff .del { background: var(--del); color: var(--del-fg); }
.diff .hunk { color: var(--s-number); background: var(--code-bg); display: inline-block; min-width: 100%; margin: .6em 0 .2em; }
.diff .meta { color: var(--fg-3); }

/* syntax (compact classes from the renderer). Scoped to highlighted code,
   which is the only place the renderer writes them: the names are one or
   two letters, and the palette's and the toast's title and subtitle spans
   are .t and .s too, which had them painted as a type and a string. */
pre.code .c { color: var(--s-comment); font-style: italic; }
pre.code .k { color: var(--s-keyword); }
pre.code .s { color: var(--s-string); }
pre.code .n { color: var(--s-number); }
pre.code .f { color: var(--s-function); }
pre.code .t { color: var(--s-type); }
pre.code .v { color: var(--s-variable); }
pre.code .p { color: var(--s-punct); }
pre.code .tg { color: var(--s-tag); }
pre.code .at { color: var(--s-attr); }
pre.code .hd { color: var(--s-keyword); font-weight: 600; }
pre.code .b { font-weight: 600; }
pre.code .i { font-style: italic; }
pre.code .raw { color: var(--s-string); }
pre.code .lnk { color: var(--accent); }
pre.code .ins { color: var(--add-fg); background: var(--add); }
pre.code .del { color: var(--del-fg); background: var(--del); }
pre.code .inv { color: var(--del-fg); }

/* ---------- inbox ---------- */
.inbox-head { margin-bottom: 28px; }
.inbox-head h1 { font-size: var(--fs-h2); letter-spacing: -.02em; font-weight: 650; margin: 0 0 4px; }
.inbox-head p { color: var(--fg-3); margin: 0; font-size: var(--fs-body-s); }
.inbox { list-style: none; margin: 0; padding: 0; }
.inbox li a { display: grid; grid-template-columns: 1fr auto; grid-template-areas: "title time" "sub sub"; gap: 2px 16px; padding: 12px 14px; margin: 0 -14px; border-radius: var(--r-md); color: inherit; border-bottom: 1px solid var(--rule); transition: background var(--t); }
.inbox li a:hover { background: var(--bg-side); text-decoration: none; }
.inbox .title { grid-area: title; font-weight: 600; font-size: var(--fs-body-s); letter-spacing: -.005em; }
.inbox .time { grid-area: time; font-size: var(--fs-small); color: var(--fg-3); font-variant-numeric: tabular-nums; }
.inbox .sub { grid-area: sub; font-size: var(--fs-small); color: var(--fg-3); }
.inbox .sub b { font-weight: 500; color: var(--fg-2); }
.inbox a.new .title::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); margin: 0 9px 2px 0; vertical-align: middle; }
.inbox-sec { display: flex; align-items: center; gap: 10px; font-size: var(--fs-micro); color: var(--fg-3); font-weight: 600; margin: 32px 0 4px; }
.inbox-sec .n { background: var(--accent); color: var(--on-accent); border-radius: var(--r-pill); padding: 0 7px; line-height: 17px; font-variant-numeric: tabular-nums; font-size: var(--fs-micro); }
.inbox-sec button { margin-left: auto; font-weight: 500; font-size: var(--fs-small); color: var(--fg-2); padding: 3px 8px; border-radius: var(--r-sm); }
.inbox-sec button + button { margin-left: 0; }
/* Removed · Show, at the foot of the Inbox, and the rows it opens. */
.inbox-sec button:hover { background: var(--rule); color: var(--fg); }
.inbox.waiting { margin-bottom: 8px; }
.empty-state { margin: 20vh auto 0; max-width: 46ch; text-align: center; color: var(--fg-2); }
.empty-state h1 { font-size: var(--fs-h3); font-weight: 600; letter-spacing: -.015em; margin: 0 0 8px; color: var(--fg); }
.empty-state p { margin: 0 0 18px; font-size: var(--fs-body-s); }
.empty-state .hero .mk { display: block; width: 44px; height: 44px; margin: 0 auto 16px; }
.empty-state pre { text-align: left; font-family: var(--mono); font-size: var(--fs-ui); background: var(--code-bg); padding: 12px 16px; border-radius: var(--r-sm); display: inline-block; margin: 0; }

/* ---------- rail ---------- */
/* Two things live in the rail and they scroll apart. The contents can be
   longer than the pane -- 46 entries at 1868 px in a 1043 px window on a plan
   with numbered sections -- and used to push the actions under it off the
   screen, so "Pin" and "Compare with previous" were a scroll away on exactly
   the documents worth pinning and comparing. The contents scroll on their own
   now, in the room the actions leave, and the actions stay put. */
#rail { padding: 60px 0 20px 8px; overflow: hidden; font-size: var(--fs-small); }
#toc { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; position: relative; padding: 14px 20px 14px 0; scroll-behavior: smooth; }
/* Entries that have scrolled out fade at the edges rather than being cut mid-line.
   The padding is the fade's height, so an unscrolled list has nothing in it. */
#toc { mask-image: linear-gradient(transparent, #000 14px, #000 calc(100% - 14px), transparent); }
#toc ul { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--rule); }
#toc li a { display: block; padding: 3px 10px; color: var(--fg-3); border-left: 1px solid transparent; margin-left: -1px; line-height: 1.4; transition: color var(--t), border-color var(--t); overflow-wrap: anywhere; }
/* Two lines of a heading, then ellipsis: better than cutting every entry mid-word. */
#toc li a { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
#toc li.d3 a { padding-left: 22px; }
#toc li.d4 a { padding-left: 34px; }
#toc li a:hover { color: var(--fg); text-decoration: none; }
#toc li a.cur { color: var(--accent); border-left-color: var(--accent); }
#toc .outline li a { display: flex; align-items: baseline; gap: 6px; }
#toc .outline .ok { width: 5px; height: 5px; border-radius: 1px; flex: none; align-self: center; background: var(--fg-3); }
#toc .outline .ok-fn { background: var(--s-function); border-radius: 50%; }
#toc .outline .ok-type { background: var(--s-type); }
#toc .outline .ok-impl { background: var(--s-keyword); transform: rotate(45deg); }
#toc .outline .ok-mod { background: var(--s-string); }
#toc .outline li a { font-family: var(--mono); font-size: var(--fs-small); }
pre.code .ln { scroll-margin-top: calc(var(--head-h) + 76px); }
pre.code .ln.flash { background: var(--accent-bg); }
/* A line the URL points at (#L12, #L12-L20) stays marked while it is being read;
   clicking the number in the gutter is what writes that URL. */
pre.code .ln.at { background: var(--accent-bg); }
pre.code .ln.at::before { opacity: 1; color: var(--accent); }
pre.code .ln::before { cursor: pointer; }

/* Wrap long lines, for logs and generated code that run off the pane. Where there
   are line numbers the continuations hang past them, so the code still lines up. */
:root[data-wrap="1"] pre.code { white-space: pre-wrap; }
:root[data-wrap="1"] pre.code code { min-width: 0; }
:root[data-wrap="1"] pre.code .ln { display: inline-block; width: 100%; box-sizing: border-box; overflow-wrap: anywhere; }
:root[data-wrap="1"] .kind-code pre.code .ln,
:root[data-wrap="1"] .kind-text pre.code .ln,
:root[data-wrap="1"] .kind-markdown pre.code:has(.ln:nth-of-type(12)) .ln { padding-left: var(--gutter); text-indent: calc(-1 * var(--gutter)); }

/* Capped and scrollable on its own, for a file with forty versions behind it:
   the contents above it must never be squeezed out of the pane entirely.
   It has a place of its own, rail.foot: the foot of the rail, under a rule,
   whatever the list above it holds -- a short list no longer pulls it up to
   the middle of the rail, and a long one scrolls above it. */
#meta { flex: none; max-height: 50%; overflow-y: auto; margin-top: auto; padding: 12px 20px 0 0; border-top: 1px solid var(--rule); color: var(--fg-3); line-height: 1.7; }
#meta:empty { padding: 0; border-top: 0; }
#meta .row { display: flex; gap: 8px; }
#meta .row b { font-weight: 500; color: var(--fg-2); min-width: 54px; flex: none; }
/* Long folder and file names wrap inside the rail rather than running off it. */
#meta .row span { min-width: 0; overflow-wrap: anywhere; }
#meta .actions { margin-top: 12px; display: flex; flex-direction: column; gap: 2px; align-items: flex-start; }
#meta .actions button, #meta .actions a { color: var(--fg-2); padding: 3px 0; }
#meta .actions button:hover, #meta .actions a:hover { color: var(--accent); text-decoration: none; }

/* ---------- what snyvi says back ----------
   A box at the bottom-right corner is a message left at an address nobody is
   looking at: the click was on a control in the left rail, or on a mark in
   the middle of a paragraph, and that is where the eye still is. So app.js
   puts it beside whatever was pressed and writes which way it points into
   `data-side`; everything that makes it read as snyvi leaning in from over
   there -- the tail, the tilt of the head, the direction it grows out of --
   hangs off that one attribute. With no attribute it is the corner, as
   before, kept for the answers that belong to nothing on screen: a document
   arriving, a stream going down.
   There is only ever one. The box is as small as the line inside it can be
   read at, because the face is the part being looked at, and `data-feel`
   says which face and what it does on landing.
   It is still `#toasts > .toast` with `.t`, `.s` and `button.act` in it,
   which is what the bench and everything else reads. Its look is in
   ui/toast.js since 1.8, which draws it and is fetched the first time
   snyvi has something to say. */
/* snyvi itself: the logo's head, at the logo's geometry, in the colours the
   accent moves -- so the answer to a change of accent arrives wearing it.
   It leans towards the thing it is answering, and how it lands is the whole
   difference between a receipt and an answer: glad hops, a wink nods, whoa
   arrives all at once, sorry shakes its head, and love lets a heart go.
   The head (`.mk`) and its feelings (`[data-feel]`) are not the toast's
   alone: the queue bar wears the same one, so the rules are the head's. */
.mk { display: block; width: 100%; height: 100%; overflow: visible; transform-origin: 50% 100%; }
.mk-body { fill: var(--mascot); }
.mk-nub { fill: var(--mascot-nub); transform-origin: 16px 5px; transform-box: view-box; }
.mk-ink { fill: var(--mascot-ink); }
.mk-line { fill: none; stroke: var(--mascot-ink); stroke-width: 2.2; stroke-linecap: round; }
.mk-love, .mk-puff, .mk-cheek { fill: var(--heart); }
.mk-shine { fill: var(--mascot-shine); }   /* the peek's alone: at 72 px the eyes keep their light */

/* A face moves only for what happens a few times a day (docs/DESIGN.md
   §7.2): something arriving, the queue emptying, a small failure. The
   answers to clicks -- rest, glad, wink -- change the face and keep still. */
[data-feel="love"] .mk { animation: mk-hop .62s var(--ease-spring) 1; }
[data-feel="whoa"] .mk { animation: mk-pop .5s cubic-bezier(.2,1.8,.4,1) 1; }
[data-feel="oops"] .mk { animation: mk-shake .46s ease-in-out 1; }
/* The antenna is the part with no face on it, so it carries the rest of the
   feeling: it swings behind a hop and droops when the answer is a no. */
[data-feel="love"] .mk-nub { animation: mk-swing .62s cubic-bezier(.3,1.5,.5,1) 1; }
[data-feel="oops"] .mk-nub { transform: translateY(1.4px) rotate(-14deg); }
.mk-puff { transform-box: view-box; transform-origin: 26px 8px; animation: mk-puff 1.5s cubic-bezier(.3,.7,.4,1) var(--dur-move) 1 both; }
@keyframes mk-hop { 0% { transform: none; } 32% { transform: translateY(-4px) scale(1.06, .94); } 62% { transform: translateY(0) scale(.95, 1.05); } 82% { transform: translateY(-1px); } 100% { transform: none; } }
@keyframes mk-pop { 0% { transform: scale(.62); } 55% { transform: scale(1.12); } 100% { transform: none; } }
@keyframes mk-shake { 0%, 100% { transform: none; } 20% { transform: rotate(-7deg); } 50% { transform: rotate(6deg); } 80% { transform: rotate(-3deg); } }
@keyframes mk-swing { 0%, 100% { transform: none; } 30% { transform: rotate(-20deg); } 65% { transform: rotate(12deg); } }
@keyframes mk-puff { from { opacity: 0; transform: translate(0, 2px) scale(.4); } 35% { opacity: 1; } to { opacity: 0; transform: translate(3px, -11px) scale(1); } }
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .toast, .mk, .mk-nub, .mk-puff { animation: none; }
}

/* ---------- palette / help ---------- */
/* A native control snyvi has not drawn itself still wears the accent. */
:root { accent-color: var(--accent); }
#palette, #help, #about, #reset { position: fixed; inset: 0; background: var(--scrim); z-index: var(--z-dialog); display: grid; place-items: start center; padding-top: 12vh; }
#palette[hidden], #help[hidden], #about[hidden], #reset[hidden] { display: none; }
.palette-box, .help-box { width: min(620px, 92vw); background: var(--bg-raise); border-radius: var(--r-md); box-shadow: var(--shadow-3); overflow: hidden; animation: rise var(--dur-move) var(--ease-out); }
/* ⌘K is pressed a hundred times a day: it is there, not arriving (§7.2). */
.palette-box { animation: none; }
#palette-input { width: 100%; font: inherit; font-size: 16px; padding: 14px 18px; border: 0; background: transparent; color: inherit; outline: none; border-bottom: 1px solid var(--rule); }
/* The palette's list is ui/palette.js's, and comes with it. */
/* The dialogs' own look -- the shortcuts card, About, Reset -- is ui/about.js's, which fills
   them and is fetched before any of them opens. */
/* Buttons (docs/DESIGN.md §8): five kinds on one base, with one hover, one
   focus ring and one disabled look. The names markup already had are the
   kinds' too: \`.w-btn\` is primary, \`button.text\` is text. */
.btn, .w-btn, button.text { display: inline-flex; align-items: center; justify-content: center; gap: 6px; font: inherit; font-size: var(--fs-ui); font-weight: 500; line-height: 1.2;
  padding: 6px 12px; border: 1px solid transparent; border-radius: var(--r-sm); color: var(--fg); background: none; cursor: pointer; }
:is(.btn, .w-btn, button.text):focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
:is(.btn, .w-btn, button.text):is(:disabled, [aria-disabled="true"]) { opacity: .45; cursor: default; }
.btn-primary, .w-btn { color: var(--on-accent); background: var(--accent); font-weight: 600; }
:is(.btn-primary, .w-btn):not(:disabled):hover { background: color-mix(in srgb, var(--accent), var(--fg) 12%); }
.btn-secondary { border-color: var(--rule-2); }
.btn-text, button.text { color: var(--fg-2); padding: 4px 8px; }
:is(.btn-secondary, .btn-text, button.text):not(:disabled):hover { color: var(--fg); background: var(--rule); }
/* Danger is an outline until it is armed, or is the one thing a dialog does. */
.btn-danger { color: var(--danger); border-color: var(--danger); }
.btn-danger:is([data-armed], .fill) { color: var(--on-accent); background: var(--danger); }
.btn-danger:is([data-armed], .fill):not(:disabled):hover { background: color-mix(in srgb, var(--danger), var(--fg) 12%); }
/* The copy button in a block of code or a command: one of them. */
.copy { position: absolute; top: 6px; right: 8px; font: 500 var(--fs-micro) var(--sans); padding: 3px 8px; border: 0; border-radius: var(--r-xs);
  background: var(--bg-raise); color: var(--fg-2); box-shadow: var(--shadow-1); cursor: pointer; }
.copy:hover { color: var(--fg); }
.copy:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
/* The reset dialog: a sentence, a number, and a button that is dead until
 * the number is right. Red is spent here and nowhere else in the chrome. */

/* A long code block comes cut into chunks of 200 lines (render::chunk_code),
   so only the chunks near the screen are laid out. The height is a guess until
   a chunk has been seen once, and `auto` remembers it after that. Only the
   block axis: a width guess would become the code's scrollable width. */
pre.code .lc { display: block; content-visibility: auto; contain-intrinsic-block-size: auto 320em; }

/* Folders is always in the sidebar, with its + for the desktop's folder dialog. */
.picking .b-empty { color: var(--accent); cursor: progress; }
.b-empty { display: block; width: 100%; height: 28px; text-align: left; padding: 0 8px 0 18px; font-size: var(--fs-small); color: var(--fg-3); border-radius: var(--r-sm); }
.b-empty:hover { background: var(--rule); color: var(--fg-2); }

/* ---------- where the mascot never is, and the quiet switch ----------
   On a desk the panels are the work: no face in a toast or the queue bar
   there (docs/DESIGN.md §2.3). The quiet switch (§2.5, ⌘K and the mark's
   menu) keeps every face at rest and nothing of it moving: no hops, blinks,
   hearts, hover lines or peek; a waiting aside is the nub, lit. */
:root[data-view="desk"] :is(.toast .who, #queue-bar .qb-who) { display: none; }

/* Working: the one mark for a process under way (docs/DESIGN.md §7.4) --
   three dots, the light walking along them, beside a word that says what.
   Content that is coming is a skeleton instead. Under reduced motion the
   dots stand still at two of three, which still reads as "under way". */
.dots { display: inline-flex; gap: 3px; margin-right: 6px; vertical-align: middle; }
.dots i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: .3; animation: dots calc(var(--dur-moment) * 2) var(--ease-out) infinite; }
.dots i:nth-child(2) { animation-delay: var(--dur-quick); }
.dots i:nth-child(3) { animation-delay: calc(var(--dur-quick) * 2); }
@keyframes dots { 30% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .dots i:nth-child(-n+2) { opacity: 1; } }
:root[data-mascot="quiet"] :is(.mk, .mk *, .brand-mark *, .note-bg) { animation: none !important; transition: none !important; }
:root[data-mascot="quiet"] :is(.bm-say, .note-bg, .uc-bg, .mk-puff, .brand-mark .bm-heart) { display: none !important; }
/* The last one waiting read: love, on the mark, once (app.js renderQueue). */
html[data-cleared] .brand-mark .bm-open { display: none; }
html[data-cleared] .brand-mark .bm-glad { display: inline; }
html[data-cleared] .brand-mark .bm-heart { opacity: 1; transform: scale(1); }
/* A desk's last open note ticked: glad, on the mark, with the hop the mark
   already has for an aside's arrival (app.js markDone). Never on the desk. */
html[data-done] .brand-mark .bm-open { display: none; }
html[data-done] .brand-mark .bm-glad { display: inline; }