snyvi 1.4.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
1374
1375
1376
1377
1378
1379
/* 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"); }

: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 */
  --bg: #faf9f6;
  --bg-side: #f3f1ec;
  --bg-raise: #ffffff;
  --fg: #1f1d1a;
  --fg-2: #5c574f;
  --fg-3: #756f66;
  --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 the dark palette. Every warm touch in the
     chrome derives from it -- washes, syntax keywords, snyvi's own face. */
  --accent-l: #8c0f26;
  --accent-d: #f04a63;
  --accent: var(--accent-l);
  --accent-bg: color-mix(in srgb, var(--accent) 10%, var(--bg));
  --brand: var(--accent-l);   /* the logo's colour, the same in both themes */
  /* 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. */
  --mascot: #d9203b;
  --mascot-nub: var(--brand);
  --mascot-ink: color-mix(in srgb, var(--brand) 40%, #000);
  --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);

  /* syntax, paper */
  --s-comment: #8a8378;
  --s-keyword: color-mix(in srgb, var(--accent) 85%, #000);
  --s-string: #3b6d11;
  --s-number: #0b6e99;
  --s-function: #6a3fb5;
  --s-type: #0e7490;
  --s-variable: #1f1d1a;
  --s-punct: #6b665e;
  --s-tag: var(--s-keyword);
  --s-attr: #6a3fb5;

  --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;
  --radius: 6px;
  --t: 140ms cubic-bezier(.2,.7,.2,1);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --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);
    --accent: var(--accent-d);
    --accent-bg: color-mix(in srgb, var(--accent) 16%, transparent);
    --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);
    --s-comment: rgba(255,255,255,.40);
    --s-keyword: var(--accent);
    --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,.55);
    --s-tag: var(--accent);
    --s-attr: #c4b5fd;
  }
}
:root[data-theme="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);
  --accent: var(--accent-d);
  --accent-bg: color-mix(in srgb, var(--accent) 16%, transparent);
  --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);
  --s-comment: rgba(255,255,255,.40);
  --s-keyword: var(--accent);
  --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,.55);
  --s-tag: var(--accent);
  --s-attr: #c4b5fd;
}
/* 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: #15803d; --accent-d: #4ade80; --mascot: #7dff9e; }
:root[data-accent="graphite"] { --accent-l: #57534e; --accent-d: #d6d3d1; --mascot: #dcd7d0; }
/* 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: 15px; 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(--accent); outline-offset: 2px; border-radius: 3px; }
::selection { background: var(--accent-bg); }

/* ---------- layout ---------- */
#app {
  display: grid; height: 100%;
  grid-template-columns: var(--side-w) minmax(0, 1fr) var(--rail-w);
  grid-template-areas: "side main rail";
}
:root[data-side="0"] #app { grid-template-columns: 0 minmax(0,1fr) var(--rail-w); }
:root[data-side="0"] #side { display: none; }
:root[data-rail="0"] #app { grid-template-columns: var(--side-w) minmax(0,1fr) 0; }
:root[data-rail="0"] #rail, #rail.empty { display: none; }
:root[data-rail="0"][data-side="0"] #app { grid-template-columns: 0 minmax(0,1fr) 0; }
#app:has(#rail.empty) { grid-template-columns: var(--side-w) minmax(0,1fr) 0; }
:root[data-side="0"] #app:has(#rail.empty) { grid-template-columns: 0 minmax(0,1fr) 0; }


#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-w) minmax(0,1fr) 0 !important; }
  #rail { display: none; position: fixed; inset: 0 0 0 auto; width: min(88vw, 320px); z-index: 20; 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 160ms cubic-bezier(.2,.7,.2,1); }
  #chrome #btn-rail { display: inline-grid; }
  #app:has(#rail.empty) #btn-rail { display: none; }
  #rail .gutter { display: none; }
}
@media (max-width: 760px)  {
  #app { grid-template-columns: 0 minmax(0,1fr) 0 !important; }
  #side { display: none; position: fixed; inset: 0 auto 0 0; width: min(88vw, var(--side-w)); z-index: 20; box-shadow: var(--shadow); }
  :root[data-sheet="side"] #side { display: flex; animation: sheet-l 160ms cubic-bezier(.2,.7,.2,1); }
  #chrome #btn-side { display: inline-grid; }
  #side .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: 15; background: rgba(20, 16, 10, .28); 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: 10; 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; }
/* A document read over a desk: its name as the bar's heading, at the left,
   and the ✕ that puts the panels back at the right, 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: 14px; font-weight: 600; color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0; transform: translateY(6px); transition: opacity .2s, transform .24s cubic-bezier(.2,.7,.2,1); }
#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: 4px; 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, :root[data-side="0"] #chrome #btn-side { 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; }
#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; }
/* The panes' own scrollbars: there when a list is longer than the pane, and
   no wider than they need to be. The document keeps the browser's. */
#trees, #toc, #meta { scrollbar-width: thin; scrollbar-color: var(--rule-2) transparent; }
@media (prefers-reduced-motion: reduce) { #main, #toc { scroll-behavior: auto; } * { 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: #f0607e; 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; }
html[data-note] .brand-mark .bm-open { animation: bm-blink 4.6s infinite; }
html[data-note="lit"] .brand-mark .bm-nub { animation: bm-ping 1.6s ease-in-out infinite; }
.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 { 0%, 100% { fill: var(--mascot-nub); } 50% { fill: color-mix(in srgb, var(--brand) 60%, #fff); } }
@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: #f0607e; }
.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 .16s ease; }
html[data-say] .brand-name { opacity: 0; }
.bm-say { position: absolute; left: 28px; top: 50%; z-index: 29;
  font-family: var(--sans); font-size: 12.5px; font-weight: 500; letter-spacing: -.005em;
  color: var(--accent); white-space: nowrap; max-width: 132px; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
  opacity: 0; transform: translateY(calc(-50% + 4px));
  transition: opacity .16s ease, transform .3s cubic-bezier(.3,1.6,.5,1); }
.bm-say.on { opacity: 1; transform: translateY(-50%); }
/* 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 beside the brand now, 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-right: 4px; padding: 1px 7px; border-radius: 999px; background: var(--rule); color: var(--fg-3); font-size: 11px; 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(--accent); background: var(--accent-bg); }
.live.on::before { opacity: 1; }
/* 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: 6px; 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); }
/* Drawn in the face it has picked, so the button says which one it is. */
.icon .aa { font-size: 13px; font-weight: 600; letter-spacing: -.02em; font-family: var(--body); }
:root[data-font="mono"] .icon .aa { font-size: 11px; letter-spacing: -.06em; }
/* The note. Nothing until an agent says something; a warm glow until a
   reader rests on it; then a single quiet line. The trail of the ones before
   opens upward on hover, over the tree rather than pushing it. */
#note { position: relative; margin: 0 8px;
  /* A side note's surface: a breath of the accent in the sidebar's colour,
     and a hairline with a little warmth in it. Quiet on purpose. */
  --note-bg: color-mix(in srgb, var(--accent) 6%, var(--bg-side));
  --note-rule: color-mix(in srgb, var(--accent) 16%, var(--rule)); }
.note-now { position: relative; display: block; padding: 9px 12px; border-radius: 10px; cursor: default; outline: none;
  background: transparent; border: 1px solid transparent; transition: background .6s ease, border-color .6s ease, box-shadow .6s ease, padding .3s ease; }
.note-now { overflow: hidden; isolation: isolate; }
.note-now > p, .note-now > .note-by { position: relative; z-index: 1; }
.note-now[data-about] { cursor: pointer; }
/* snyvi peeking from behind the note on hover, and for a moment when a new one arrives: large, tilted, faint, with a
   feeling. It rises from the corner rather than fading in on the spot. */
.note-bg { position: absolute; right: -12px; bottom: -22px; width: 72px; height: 72px; z-index: 0; pointer-events: none;
  opacity: 0; transform: translate(12px, 26px) rotate(0deg); transition: opacity .25s ease, transform .45s cubic-bezier(.3,1.5,.5,1); }
#note:hover .note-bg, #note:focus-within .note-bg, #note.peek .note-bg { opacity: .16; transform: rotate(-14deg); transition-delay: .12s; }
.note-bg .nb-body { fill: var(--mascot); }
.note-bg .nb-nub { fill: var(--mascot-nub); }
.note-bg .nb-cheek, .note-bg .nb-love { fill: #f0607e; }
.note-bg .nb-ink { fill: var(--mascot-ink); }
.note-bg .nb-shine { fill: #fff; }
.note-bg .nb-line { fill: none; stroke: var(--mascot-ink); stroke-width: 2; stroke-linecap: round; }
:root[data-theme="light"] #note:hover .note-bg, :root[data-theme="light"] #note:focus-within .note-bg, :root[data-theme="light"] #note.peek .note-bg { opacity: .22; }
/* Opened -- hovered or focused -- a read note wears the same warm card as
   the ones in the trail above it, so the stack reads as one voice. */
#note:hover .note-now, #note:focus-within .note-now { background: var(--note-bg); border-color: var(--note-rule); }
/* The surface is quiet, so the words are not. */
#note:hover .note-now p, #note:focus-within .note-now p { color: var(--fg); }
/* snyvi peeks from the byline's corner, so the byline ends before it does. */
#note:hover .note-by-now, #note:focus-within .note-by-now, #note.peek .note-by-now { padding-right: 40px; }
.note-now:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
/* Interface text, not reading text: the UI's sans at 13px for the note and
   11px for its byline, the sizes a Mac sidebar uses for a label and its
   caption. The reading font is for documents; at 13px in a sidebar a serif
   goes soft. */
.note-now p, .note-trail p { margin: 0; font-family: var(--sans); font-size: 13px; font-weight: 400; line-height: 1.38; letter-spacing: -.003em;
  color: var(--fg-2); text-wrap: pretty; }
.note-by { display: block; margin-top: 4px; font-family: var(--sans); font-size: 11px; line-height: 1.3; color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.note-by-now { display: flex; gap: 6px; }
.note-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.note-snyvi { font-weight: 600; color: var(--accent); }
.note-more { flex: none; margin-left: auto; padding: 0 5px; border-radius: 6px; background: var(--rule); color: var(--fg-3); }
/* On hover the trail it counts is open above, and snyvi is peeking from that corner. */
#note:hover .note-more, #note:focus-within .note-more, #note.peek .note-more { visibility: hidden; }
#note[data-lit="1"] .note-now { background: var(--accent-bg); border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  animation: note-glow 3.2s ease-in-out infinite; }
#note[data-lit="1"] .note-now p { color: var(--fg); }
#note[data-seen="1"]:not(:hover):not(:focus-within) .note-now p { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg-3); }
#note[data-seen="1"]:not(:hover):not(:focus-within) .note-by { display: none; }
#note[data-seen="1"]:not(:hover):not(:focus-within) .note-now { padding-top: 5px; padding-bottom: 5px; }

@keyframes note-glow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
  50% { box-shadow: 0 0 14px 0 color-mix(in srgb, var(--accent) 16%, transparent); }
}
/* The ones before, each its own card in the same quiet surface as the note
   below them, opaque over the tree they float above. */
.note-trail { position: absolute; left: 0; right: 0; bottom: 100%; margin: 0 0 6px; padding: 0; list-style: none; z-index: 5;
  display: flex; flex-direction: column; gap: 6px;
  opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s; }
#note:hover .note-trail, #note:focus-within .note-trail { opacity: 1; visibility: visible; transform: none; transition-delay: .25s, .25s, 0s; }
.note-trail li { padding: 9px 12px; border-radius: 10px; background: var(--note-bg); border: 1px solid var(--note-rule); box-shadow: 0 4px 14px rgba(0,0,0,.12); }
.note-trail li[data-about] { cursor: pointer; }
.note-trail li[data-about]:hover { border-color: color-mix(in srgb, var(--accent) 35%, var(--rule)); }
.note-trail p { color: var(--fg); }
.note-trail .note-by { margin-top: 4px; }

.side-foot { display: flex; align-items: center; gap: 4px; padding: 8px 12px 10px; margin-top: auto; }
.side-foot { position: relative; }
/* 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 .34s cubic-bezier(.3,1.5,.5,1) 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. A hairline and no fill:
   the sidebar behind it is quiet enough to read the column against.
   `.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: 28;
  display: flex; flex-direction: column-reverse; align-items: center; gap: 2px; padding: 5px;
  border-radius: 10px; border: 1px solid 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 .18s ease; }
.foot-rail .keep { pointer-events: auto; }
.foot-set:hover .foot-rail, .foot-set:focus-within .foot-rail { border-color: var(--rule); 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 .14s ease, transform .2s 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; }
.foot-rail > button:nth-child(2) { transition-delay: .02s; }
.foot-rail > button:nth-child(3) { transition-delay: .05s; }
.foot-rail > button:nth-child(4) { transition-delay: .08s; }
.foot-rail > button:nth-child(5) { transition-delay: .11s; }
.foot-rail > button:nth-child(6) { transition-delay: .14s; }
.foot-rail > button:nth-child(7) { transition-delay: .17s; }
/* What each one is, said beside it. These were `title`s, and the browser drew
   them where it liked -- over the column, on its own schedule, covering the
   very icons a reader was looking down to read. The name belongs next to the
   column, not on top of it, and there is room to the right for all of them.
   `aria-label` is what a screen reader was using all along; only the drawing
   of it has changed. */
.foot-rail > button { position: relative; }
.foot-rail > button::after { content: attr(data-label);
  position: absolute; left: calc(100% + 9px); top: 50%; transform: translate(-3px, -50%); z-index: 32;
  padding: 3px 8px; border-radius: 7px; background: var(--bg-raise); border: 1px solid var(--rule);
  font-family: var(--sans); font-size: 11px; line-height: 1.35; font-weight: 500; color: var(--fg-2);
  white-space: nowrap; pointer-events: none; opacity: 0;
  transition: opacity .12s ease, transform .18s ease; }
.foot-rail > button:hover::after, .foot-rail > button:focus-visible::after { opacity: 1; transform: translateY(-50%); }
/* snyvi answers a press in that same strip of space to the right, which is
   the point of answering there -- so while it is talking, the label steps
   aside rather than being talked over. */
.foot-rail > button.said::after { opacity: 0 !important; }
/* And the column holds itself open for as long as it is being answered: the
   pointer has usually left by then, and a tail pointing at the space where a
   button used to be is worse than no tail at all. */
.foot-rail:has(> button.said) { border-color: var(--rule); pointer-events: auto; }
.foot-rail:has(> button.said) > button { opacity: 1; transform: none; pointer-events: auto; }
/* Nothing hovers on a touch screen, so there the row stays as it was. */
@media (hover: none) {
  .foot-set { width: auto; height: auto; }
  .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: 14px; font-weight: 600; }
/* The rocket's game covers the sidebar and nothing else: absolute over the
   column, above the note's trail and the rail (28), under the palette and
   the dialogs (40), so a document opening beside it is never in its way and
   a dialog over it is still a dialog. It is `ui/game.js`'s to draw; this is
   only its shell -- a head with the score, the sky, a foot with the best. */
#side .game { position: absolute; inset: 0; z-index: 30; display: flex; flex-direction: column; background: var(--bg-side); }
.game-head, .game-foot { display: flex; align-items: center; gap: 8px; font-family: var(--sans); color: var(--fg-3); }
.game-head { padding: 12px 10px 4px 16px; font-size: 12px; }
.game-title { margin-right: auto; font-weight: 600; color: var(--fg-2); letter-spacing: -.005em; }
.game-score { font-family: var(--mono); font-size: 13px; font-variant-numeric: tabular-nums; color: var(--fg); margin-right: 2px; }
.game-sky { flex: 1; min-height: 0; width: 100%; display: block; touch-action: none; }
/* An outline drawn inwards, not an inset shadow: the canvas paints its own
   bitmap over the element's shadow every frame, so the ring was there and
   then covered. The sky is the game's only focusable control and it takes
   every flight key, so this is the one ring that has to show. */
.game-sky:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.game-foot { justify-content: space-between; padding: 4px 16px 12px; font-size: 11px; }
.game-hint, .game-best { white-space: nowrap; }
.game-best { font-family: var(--mono); font-variant-numeric: tabular-nums; }

#trees { overflow-y: auto; padding: 0 8px 8px; font-size: 13px; 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; }
.s-link { flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px; padding: 0 8px; font: inherit; font-size: 11px; font-weight: 600; letter-spacing: .01em; color: var(--fg-3); background: none; border-radius: 6px; 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: 5px; opacity: 0; transition: opacity var(--t), background var(--t), color var(--t); }
.s-head:hover .s-add, .s-add:focus-visible, #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: #b45309; font-size: 11px; font-weight: 700; }
#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: 12px; 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: 6px; color: var(--fg); font-weight: 450; 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; }
.s-row.active, .t-desk > a.active { background: var(--accent-bg); 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: 11px; 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); }
.t-proj > summary::-webkit-details-marker { display: none; }
/* 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: 11px; 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: 6px; 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(--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: 550; }
.t-queue { margin: 0 0 6px; }
.t-queue .t-label { display: flex; align-items: center; padding-top: 2px; }
.t-queue .t-label .n { margin-left: auto; background: var(--accent); color: #fff; border-radius: 9px; padding: 0 6px; line-height: 16px; letter-spacing: 0; font-size: 10px; }
.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: 11px; color: var(--fg-3); padding: 3px 6px; border-radius: 5px; }
.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. */
.t-doc.wash { animation: land 700ms ease-out; border-radius: 6px; }   /* on the row itself, where its moment is */
.t-doc.leaving, .t-queue.leaving .t-label { animation: leave 140ms cubic-bezier(.2,.7,.2,1) forwards; overflow: hidden; pointer-events: none; }
@keyframes leave { from { opacity: 1; max-height: 32px; } to { opacity: 0; max-height: 0; } }
.t-doc .k { margin-left: auto; font-size: 10px; color: var(--fg-3); font-family: var(--mono); flex: none; font-variant-numeric: tabular-nums; }
.t-doc .pin { color: var(--accent); font-size: 7px; flex: none; 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: 11px; line-height: 1; padding: 2px 4px; border-radius: 4px; 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: 4px; padding: 1px 4px; }
.ren-in:focus { outline: none; }
/* 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: 11px; color: var(--fg-3); background: none; border: 0; padding: 3px 6px; border-radius: 5px; 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: 12px; color: var(--fg-3); }
.t-empty { padding: 24px 12px; color: var(--fg-3); font-size: 13px; line-height: 1.6; }
.t-empty code { font-family: var(--mono); font-size: 12px; background: var(--rule); padding: 1px 5px; border-radius: 4px; }

/* 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. */
.t-desks .end { display: flex; align-items: center; gap: 4px; flex: none; margin-left: auto; }
.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: #16a34a; }
.t-desks .dot.blk { color: #b45309; font-size: 11px; font-weight: 700; line-height: 1; }
.t-desks .k { min-width: 2ch; text-align: right; font-size: 10px; font-family: var(--mono); color: var(--fg-3); 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: 11px; line-height: 1; color: var(--fg-3); background: none; border-radius: 4px; cursor: pointer; }
.t-proj > summary:hover .row-x, .row-x:focus-visible, .arming .row-x { width: auto; margin-left: auto; padding: 2px 4px; overflow: visible; }
.t-proj > summary > .row-x.row-x { margin-left: 0; }   /* the ✎ before it already took the free space */
/* 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, :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); }
:is(.t-doc, .t-desk) > a:is(:hover, :has(:focus-visible)) :is(.end, .k, .pin), .arming > a :is(.end, .k), .t-desk > a:has(> [data-armed]) > .ren { visibility: hidden; }
/* 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: 11px; color: var(--fg-3); border-radius: 6px; }
.t-away:hover { background: var(--rule); 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 { color: var(--fg-3); font-size: 12px; }
.t-undo { flex: none; font-size: 11px; line-height: 1; padding: 3px 7px; border-radius: 4px; color: var(--accent); }
.t-undo:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.row-x[data-armed] { color: #dc2626; font-weight: 600; }
.row-x[data-armed]:hover { background: color-mix(in srgb, #dc2626 12%, transparent); color: #dc2626; }
:is(.t-doc, .t-desk) > a > .row-x[data-armed]:hover { background: linear-gradient(color-mix(in srgb, #dc2626 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: 4px; 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:hover > .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; }
#ctx { position: fixed; z-index: 40; min-width: 200px; padding: 4px; background: var(--bg-raise); border: 1px solid var(--rule); border-radius: 8px; box-shadow: var(--shadow); font-size: 13px; }
#ctx button { display: block; width: 100%; text-align: left; padding: 5px 10px; border-radius: 5px; color: var(--fg-2); }
#ctx button:hover, #ctx button:focus { background: var(--accent-bg); color: var(--accent); outline: none; }
#ctx hr { border: 0; border-top: 1px solid var(--rule); margin: 4px 2px; }

/* browse: folders read straight from disk */
.t-label { padding: 8px 10px 2px; font-size: 10px; letter-spacing: .09em; text-transform: uppercase; color: var(--fg-3); font-weight: 600; }
.b-root > summary::-webkit-details-marker { display: none; }
.b-root > summary:hover { background: var(--rule); color: var(--fg); }
.b-close { margin-left: auto; color: var(--fg-3); font-size: 11px; line-height: 1; padding: 2px 4px; border-radius: 4px; opacity: 0; }
.b-root > summary:hover .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: 12px; font-style: italic; }
.b-dir > details > summary { list-style: none; cursor: pointer; padding: 3px 8px; border-radius: 6px; color: var(--fg-2); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.b-dir > details > summary::-webkit-details-marker { display: none; }
.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: 6px; 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(--accent); }
.b-file .title { overflow: hidden; text-overflow: ellipsis; }
.b-file .k { margin-left: auto; font-size: 10px; color: var(--fg-3); 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(--radius); background: #fff; display: block; }

.browse-image, .doc-image { text-align: center; margin: 1.5em 0; }
.browse-image img, .doc-image img { border-radius: var(--radius); 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: 34px; 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: 13px; margin: 0; }
.doc-sub .branch { font-family: var(--mono); font-size: 12px; }

/* ---------- 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: 5px;
  background: var(--fg); opacity: 0;
  animation: sk-in .28s ease var(--sk-wait) forwards, 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); }
@keyframes sk-in { to { opacity: .07; } }
@keyframes sk-breathe { 50% { opacity: .13; } }
@media (prefers-reduced-motion: reduce) {
  /* The waiting still has to show; only the breathing goes. */
  .sk-bar { animation: sk-in .28s ease var(--sk-wait) forwards; }
}

.prose { font-family: var(--body); font-size: 17px; line-height: 1.65; }
:root[data-font="serif"] .prose { font-size: 18px; }
:root[data-font="mono"] .prose { font-size: 15px; 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: 4px; 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 #fff; 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(--radius); display: block; margin: 1.5em auto; }
.prose strong { font-weight: 650; }
.prose mark { background: var(--mark); color: inherit; padding: 0 .15em; border-radius: 2px; }
.prose kbd { font-family: var(--mono); font-size: .8em; border: 1px solid var(--rule-2); border-bottom-width: 2px; border-radius: 4px; 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(--radius); 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(--radius); 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: #3b82f6; } .prose .markdown-alert-note .markdown-alert-title { color: #3b82f6; }
.prose .markdown-alert-tip { border-color: #22c55e; } .prose .markdown-alert-tip .markdown-alert-title { color: #16a34a; }
.prose .markdown-alert-important { border-color: #a855f7; } .prose .markdown-alert-important .markdown-alert-title { color: #9333ea; }
.prose .markdown-alert-warning { border-color: #f59e0b; } .prose .markdown-alert-warning .markdown-alert-title { color: #d97706; }
.prose .markdown-alert-caution { border-color: #ef4444; } .prose .markdown-alert-caution .markdown-alert-title { color: #dc2626; }

/* inline code */
.prose code { font-family: var(--mono); font-size: .86em; background: var(--code-bg); padding: .12em .38em; border-radius: 4px; 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: 13.5px; line-height: 1.6; background: var(--code-bg); border-radius: var(--radius); 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: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); opacity: 0; transition: opacity var(--t); pointer-events: none; }
pre.code:hover::before { opacity: 1; }
pre.code .copy { position: absolute; top: 6px; right: 8px; opacity: 0; transition: opacity var(--t); font-family: var(--sans); font-size: 11px; padding: 3px 8px; border-radius: 4px; background: var(--bg-raise); color: var(--fg-2); box-shadow: 0 1px 2px rgba(0,0,0,.12); }
pre.code:hover .copy { opacity: 1; }
pre.code:hover:has(.copy)::before { opacity: 0; }
pre.code .copy:hover { color: var(--fg); }
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-desk > a > .ren, .t-proj > summary > .row-x { width: auto; padding: 2px 4px; margin-left: 0; }
  .t-desk > a > .ren { position: static; translate: none; background: none; pointer-events: 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: 13px; }
.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) { 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: 10px 48px 0; box-sizing: border-box; font-size: 13px; }
/* 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. */
#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 { 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: 8px; padding: 5px 5px 5px 9px; animation: rise 160ms cubic-bezier(.2,.7,.2,1); }
/* 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. */
#queue-bar .qb-who { width: 21px; height: 21px; 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 180ms cubic-bezier(.2,.7,.2,1); }
@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: 550; color: var(--fg); }
#queue-bar button:not(.icon), #queue-bar .qb-all { flex: none; padding: 4px 10px; border-radius: 6px; 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; }
#queue-bar kbd { font-family: var(--mono); font-size: 10px; color: var(--fg-3); margin-left: 6px; border: 1px solid var(--rule-2); border-radius: 3px; padding: 0 4px; line-height: 14px; display: inline-block; }
#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: 14px; padding: 7px 12px; border: 1px solid var(--rule-2); border-radius: 6px; color: inherit; outline: none; box-shadow: var(--shadow); }
#find-input:focus { border-color: var(--accent); }
#find-count { font-size: 12px; 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: 2px; }
mark.find.cur { background: var(--accent); color: #fff; }

/* split diff */
.split { display: grid; grid-template-columns: 1fr 1fr; font-family: var(--mono); font-size: 13px; line-height: 1.6; border: 1px solid var(--rule); border-radius: var(--radius); overflow-x: auto; }
.split > div { padding: 0 10px; white-space: pre; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.split .l { border-right: 1px solid var(--rule); }
.split .full { grid-column: 1 / -1; }
.split .meta { color: var(--fg-3); padding-top: .4em; }
.split .hunk { color: var(--s-number); background: var(--code-bg); margin: .4em 0 .2em; }
.split .add { background: var(--add); color: var(--add-fg); }
.split .del { background: var(--del); color: var(--del-fg); }
.split .add mark { background: rgba(34,197,94,.35); color: inherit; border-radius: 2px; }
.split .del mark { background: rgba(239,68,68,.35); color: inherit; border-radius: 2px; }
.split .empty { background: repeating-linear-gradient(45deg, transparent 0 6px, var(--rule) 6px 7px); }
#doc:has(.split) { max-width: none; padding-left: 40px; padding-right: 40px; }

/* 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: 12.5px; color: var(--fg-3); white-space: pre-wrap; }
pre.mermaid::before { display: none; }

/* A drawn diagram is not prose, so it takes what the reading measure is not
   using: the column's own padding, which is as much as can be given without
   reflowing the text around it. The pane itself is one keystroke away (`w`) and
   the screen is another (`f`). */
.mmd { margin: 1.5em 0; position: relative; }
.mmd[data-state="done"] { margin-inline: calc(-1 * var(--doc-pad, 0px)); }
.mmd-frame { display: flex; align-items: center; justify-content: center; }
.mmd-frame svg { max-width: 100%; height: auto; }
/* Once it has a viewport the frame owns the box and the SVG fills it, so the
   viewBox is the only thing that decides what is shown. */
.mmd[data-zoom] .mmd-frame { overflow: hidden; border-radius: var(--radius); }
.mmd[data-zoom] .mmd-frame svg { width: 100%; height: 100%; max-width: none; display: block; }
.mmd[data-zoom="in"] .mmd-frame { cursor: grab; }
.mmd[data-grab] .mmd-frame { cursor: grabbing; }
/* The controls: present when the diagram is under the cursor or holds the
   focus, absent from a page being read -- the bargain the rename pencils in the
   tree make. Always there for a reader on a touch screen, which has no hover. */
.mmd-tools {
  position: absolute; top: 8px; right: 8px; display: flex; gap: 2px; padding: 3px;
  border-radius: var(--radius); background: var(--bg-raise); box-shadow: 0 1px 3px rgba(0,0,0,.18);
  opacity: 0; transition: opacity .12s ease; pointer-events: none;
}
.mmd:hover .mmd-tools, .mmd:focus-within .mmd-tools, .mmd[data-full] .mmd-tools { opacity: 1; pointer-events: auto; }
@media (hover: none) { .mmd-tools { opacity: 1; pointer-events: auto; } }
.mmd-tools button {
  font-family: var(--sans); font-size: 12px; line-height: 1; color: var(--fg-2);
  min-width: 26px; height: 24px; padding: 0 6px; border-radius: 5px; background: none;
}
.mmd-tools button:hover { background: var(--rule); color: var(--fg); }
.mmd-tools button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Filling the screen is the one place a 4738 px graph is legible. The figure
   is laid over the page from where it is, and the document -- not the figure
   -- is what asks the browser for fullscreen. A figure of its own in the top
   layer is what WebKitGTK, the Linux window's engine, drew every glyph of as
   nothing: the boxes and arrows stayed and the labels, the tool bar and an
   SVG's own text went; and on the way back it kept the figure at the size
   its placeholder had until the next scroll. A fixed box has neither fault in
   any engine. mmdFull in app.js. */
.mmd[data-full] { position: fixed; inset: 0; z-index: 30; margin: 0; background: var(--bg); display: flex; align-items: center; justify-content: center; }
.mmd[data-full] .mmd-frame { height: 100vh !important; width: 100vw; border-radius: 0; }
/* Until the diagram lands the figure holds about the space it will want, so the
   text below it does not jump when it does. An estimate from the source, set by
   the client -- see mmdWeight in app.js. */
.mmd:not([data-state="done"]) .mmd-frame {
  min-height: var(--mmd-reserve, 200px);
  border-radius: var(--radius);
  background: linear-gradient(var(--code-bg), var(--code-bg));
}
.mmd[data-state="error"] .mmd-frame { min-height: 0; display: block; background: none; }
.mmd-fail { padding: 2px 0; }
.mmd-note { font-family: var(--sans); font-size: 12px; letter-spacing: .04em; color: var(--fg-3); text-align: center; padding: 10px; }
/* The label is for a diagram that is about to appear; a reader who waits long
   enough to wonder gets a spinner instead of a word that never changes. */
.mmd-slow .mmd-note::before {
  content: ""; display: block; width: 18px; height: 18px; margin: 0 auto 8px;
  border: 2px solid var(--rule-2); border-top-color: var(--fg-3); border-radius: 50%;
  animation: mmd-spin .7s linear infinite;
}
@keyframes mmd-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .mmd-slow .mmd-note::before { animation-duration: 2.4s; } }
/* Over the cap: offered, not spent. */
.mmd-ask { display: block; margin: 0 auto 8px; font-family: var(--sans); font-size: 13px; padding: 7px 14px; border-radius: var(--radius); background: var(--bg-raise); color: var(--fg); box-shadow: 0 1px 2px rgba(0,0,0,.12); }
.mmd-ask:hover { color: var(--accent); }
.mmd-why { display: block; }
/* A source Mermaid could not parse is the one the reader most wants to read. */
.mmd-err { font-family: var(--sans); font-size: 13px; color: var(--del-fg); margin: 0 0 8px; text-align: left; }
/* Ligatures off, like every other code block: this is shown so the reader can
   see the characters the agent actually wrote, and JetBrains Mono draws `-->`
   as a single arrow, which is the one thing it must not do here. */
.mmd-src { font-feature-settings: "calt" 0, "liga" 0; font-family: var(--mono); font-size: 12.5px; line-height: 1.6; text-align: left; white-space: pre-wrap; background: var(--code-bg); border-radius: var(--radius); padding: 12px 14px; margin: 0; color: var(--fg-2); overflow-x: auto; }

/* history */
#history { margin-top: 20px; }
#history h4 { margin: 0 0 4px; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; 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); }
.compare-note { color: var(--fg-3); font-size: 13px; margin: -28px 0 28px; }

/* 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: 26px; letter-spacing: -.02em; font-weight: 650; margin: 0 0 4px; }
.inbox-head p { color: var(--fg-3); margin: 0; font-size: 14px; }
.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: 8px; 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: 550; font-size: 15.5px; letter-spacing: -.005em; }
.inbox .time { grid-area: time; font-size: 12px; color: var(--fg-3); font-variant-numeric: tabular-nums; }
.inbox .sub { grid-area: sub; font-size: 12.5px; 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: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3); font-weight: 600; margin: 30px 0 4px; }
.inbox-sec .n { background: var(--accent); color: #fff; border-radius: 9px; padding: 0 7px; line-height: 17px; letter-spacing: 0; font-size: 11px; }
.inbox-sec button { margin-left: auto; text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 12.5px; color: var(--fg-2); padding: 3px 8px; border-radius: 5px; }
.inbox-sec button + button { margin-left: 0; }
.inbox-sec button:hover { background: var(--rule); color: var(--fg); }
.inbox-sec kbd { font-family: var(--mono); font-size: 10px; color: var(--fg-3); margin-left: 6px; border: 1px solid var(--rule-2); border-radius: 3px; padding: 0 4px; line-height: 14px; display: inline-block; }
.inbox.waiting { margin-bottom: 8px; }
/* The connect page: one row per agent, the state as a dot and a word, and
 * everything to paste in a block with its own Copy. It sits in the document
 * pane at the document's measure, so it reads as a page and not as chrome. */
.connect .agents { list-style: none; margin: 0; padding: 0; }
.agent { padding: 16px 0; border-top: 1px solid var(--rule); }
.agent:last-child { border-bottom: 1px solid var(--rule); }
.agent-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.agent-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--fg-3); align-self: center; flex: none; }
.agent.is-connected .agent-dot { background: var(--add-fg); }
/* Here now: the accent, with a ring, so an open session reads apart from one that is only set up. */
.agent.is-live .agent-dot { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
.agent.is-live .agent-state { color: var(--accent); }
.agent.is-stale .agent-dot { background: var(--del-fg); }
.agent-name { font-size: 16px; font-weight: 600; }
.agent-state { color: var(--fg-3); font-size: 13px; }
.agent-say { margin: 6px 0 0; font-size: 14.5px; color: var(--fg-2); line-height: 1.5; }
.connect code { font-family: var(--mono); font-size: 12.5px; }
.agent-fix { margin-top: 10px; }
.agent-instr { margin: 8px 0 0; font-size: 13.5px; color: var(--fg-3); }
.connect-line { margin-top: 28px; }
.connect-line p { margin: 0 0 8px; font-size: 14px; color: var(--fg-2); }
pre.cmd { position: relative; font-family: var(--mono); font-size: 13px; line-height: 1.55; background: var(--code-bg); border-radius: var(--radius); padding: 8px 72px 8px 12px; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
pre.cmd code { background: none; padding: 0; font-size: inherit; }
pre.cmd .copy { position: absolute; top: 6px; right: 8px; font: inherit; font-family: var(--sans); font-size: 11px; padding: 3px 8px; border-radius: 4px; background: var(--bg-raise); color: var(--fg-2); box-shadow: 0 1px 2px rgba(0,0,0,.12); border: 0; cursor: pointer; }
pre.cmd .copy:hover { color: var(--fg); }
pre.cmd .copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.agent-fix details { margin-top: 8px; }
.agent-fix summary { cursor: pointer; font-size: 13px; color: var(--fg-3); }
.agent-fix details[open] summary { margin-bottom: 6px; }
.connect-foot { margin-top: 24px; color: var(--fg-3); font-size: 13.5px; }
.empty-state { margin: 20vh auto 0; max-width: 46ch; text-align: center; color: var(--fg-2); }
.empty-state h1 { font-size: 22px; font-weight: 600; letter-spacing: -.015em; margin: 0 0 8px; color: var(--fg); }
.empty-state p { margin: 0 0 18px; font-size: 15px; }
.empty-state pre { text-align: left; font-family: var(--mono); font-size: 13px; background: var(--code-bg); padding: 12px 16px; border-radius: var(--radius); 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: 12.5px; }
#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: 11.5px; }
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. */
#meta { flex: none; max-height: 50%; overflow-y: auto; margin-top: 28px; padding-right: 20px; color: var(--fg-3); line-height: 1.7; }
#toc:empty + #meta { margin-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; }
#meta .actions kbd { font-family: var(--mono); font-size: 10px; color: var(--fg-3); margin-left: 8px; border: 1px solid var(--rule-2); border-radius: 3px; padding: 0 4px; line-height: 14px; display: inline-block; }

/* ---------- 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. */
#toasts { position: fixed; right: 20px; bottom: 20px; display: flex; flex-direction: column; z-index: 30;
  align-items: flex-end; pointer-events: none; }
#toasts[data-side="right"], #toasts[data-side="below"] { align-items: flex-start; }
.toast { pointer-events: auto; position: relative;
  background: var(--bg-raise); color: var(--fg); border: 1px solid var(--rule); border-radius: 11px;
  box-shadow: var(--shadow); padding: 6px 12px 6px 7px; font-size: 12.5px; line-height: 1.3;
  display: flex; align-items: center; gap: 8px; cursor: pointer; max-width: 288px;
  animation: toast-in 260ms cubic-bezier(.2,1.3,.4,1); transform-origin: var(--from, 50% 100%); }
.toast.out { opacity: 0; transform: scale(.97); transition: opacity 160ms ease, transform 160ms ease; }
@keyframes toast-in { from { opacity: 0; transform: translate(var(--dx, 0), var(--dy, 8px)) scale(.92); } to { opacity: 1; transform: none; } }
#toasts[data-side="right"] .toast { --dx: -12px; --dy: 0; --from: 0 50%; }
#toasts[data-side="left"]  .toast { --dx: 12px;  --dy: 0; --from: 100% 50%; }
#toasts[data-side="below"] .toast { --dx: 0; --dy: -10px; --from: 22px 0; }
#toasts[data-side] .toast::before { content: ""; position: absolute; width: 8px; height: 8px;
  background: var(--bg-raise); border: 1px solid var(--rule); transform: rotate(45deg); }
#toasts[data-side="right"] .toast::before { left: -5px; top: 13px; border-top: 0; border-right: 0; }
#toasts[data-side="left"]  .toast::before { right: -5px; top: 13px; border-bottom: 0; border-left: 0; }
#toasts[data-side="below"] .toast::before { top: -5px; left: 16px; border-bottom: 0; border-right: 0; }
.toast .say { min-width: 0; }
.toast .t { font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toast .s { color: var(--fg-3); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The one thing an answer offers that has to be reached on purpose. */
.toast .act { font: inherit; font-size: 11.5px; font-weight: 550; color: var(--accent); background: transparent; border: 1px solid var(--rule-2); border-radius: 6px; padding: 3px 9px; margin-left: 4px; cursor: pointer; flex: none; }
.toast .act:hover { background: var(--bg-side); }
.toast .act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* 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. */
.toast .who { width: 23px; height: 23px; flex: none; display: block; }
.mk { display: block; width: 100%; height: 100%; overflow: visible; transform-origin: 50% 100%; }
#toasts[data-side="right"] .toast .who { transform: rotate(-7deg); }
#toasts[data-side="left"] .toast .who { transform: rotate(7deg); }
.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 { fill: #f0607e; }

[data-feel="plain"] .mk, [data-feel="wink"] .mk { animation: mk-nod .52s cubic-bezier(.3,1.6,.5,1) 1; }
[data-feel="glad"] .mk, [data-feel="love"] .mk { animation: mk-hop .62s cubic-bezier(.3,1.7,.5,1) 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="glad"] .mk-nub, [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) .18s 1 both; }
@keyframes mk-nod { 0%, 100% { transform: none; } 45% { transform: translateY(-1.6px) scale(1.03, .98); } }
@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 ---------- */
#palette, #help, #about, #reset { position: fixed; inset: 0; background: rgba(20, 16, 10, .28); z-index: 40; 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: 10px; box-shadow: var(--shadow); overflow: hidden; animation: rise 140ms cubic-bezier(.2,.7,.2,1); }
#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); }
#palette-list { list-style: none; margin: 0; padding: 6px; max-height: 50vh; overflow-y: auto; }
#palette-list:empty { display: none; }
#palette-list li { padding: 8px 12px; border-radius: 6px; cursor: pointer; display: grid; gap: 1px; }
#palette-list li.sel, #palette-list li:hover { background: var(--accent-bg); }
#palette-list .t { font-weight: 550; font-size: 14px; }
#palette-list .s { font-size: 12px; color: var(--fg-3); }
#palette-list .snip { font-size: 12.5px; color: var(--fg-2); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#palette-list mark { background: var(--mark); color: inherit; border-radius: 2px; }
/* A search with nothing in it: snyvi says so, in the list's own row shape. */
.pal-none { display: flex; align-items: center; gap: 10px; padding: 9px 12px; font-size: 13px; color: var(--fg-3); }
.pal-none .mk { width: 22px; height: 22px; flex: none; }
.pal-none b { font-weight: 550; color: var(--fg-2); }
/* The overlay places the box 12vh down, so the cap leaves that and a little
   below it: a box that outgrew the window would put its foot -- About,
   Connect, Reset -- past the bottom edge, where no click can reach it. */
.help-box { padding: 18px 22px 20px; position: relative; outline: none; max-height: 84vh; overflow-y: auto; }
.help-close { position: absolute; top: 12px; right: 12px; }
.help-box h2 { margin: 0 0 12px; font-size: 14px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); }
.help-box dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; margin: 0; font-size: 14px; }
.help-box dt { font-family: var(--mono); font-size: 12.5px; color: var(--fg-2); }
.help-box dd { margin: 0; }
.help-foot { display: flex; align-items: center; gap: 4px; }
.help-gap { flex: 1; }

/* The shortcuts card. A title row and the foot stay put; only the middle
   scrolls, and on a normal screen it never has to. Each row is the meta
   panel's own shape -- what it does, then the key, small and flat -- so
   there is no key column to line up; the keys sit on the right edge. */
#help .help-box { width: min(700px, 92vw); padding: 0; display: flex; flex-direction: column; overflow: hidden; border-radius: 12px; }
.help-head { flex: none; display: flex; align-items: center; padding: 14px 14px 12px 24px; }
.help-head h2 { margin: 0; flex: 1; font-size: 15px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--fg); }
#help .help-close { position: static; }
.help-body { flex: 1; min-height: 0; overflow-y: auto; display: grid; grid-template-columns: 1fr 1fr; gap: 0 40px; padding: 4px 24px 8px; scrollbar-width: thin; scrollbar-color: var(--rule-2) transparent; }
.help-col { display: flex; flex-direction: column; gap: 18px; align-content: start; }
.help-col h3 { margin: 0 0 2px; font-size: 11px; font-weight: 600; letter-spacing: .01em; color: var(--fg-3); }
.hk { display: flex; align-items: center; gap: 12px; min-height: 30px; padding: 1px 0; border-top: 1px solid var(--rule); font-size: 13.5px; color: var(--fg); }
.hk:first-of-type { border-top: 0; }
.hk > span:first-child { flex: 1; min-width: 0; line-height: 1.3; }
.hk em { font-style: normal; font-size: 12px; color: var(--fg-3); }
.hk .keys { flex: none; display: inline-flex; align-items: center; gap: 3px; font-size: 12px; color: var(--fg-3); }
.hk .keys i { font-style: normal; font-size: 11px; padding: 0 1px; }
.hk .keys code { font-family: var(--mono); font-size: 11px; color: var(--fg-2); background: var(--rule); padding: 0 5px; border-radius: 4px; line-height: 18px; }
#help kbd { display: inline-block; box-sizing: border-box; min-width: 20px; padding: 0 5px; font-family: var(--mono); font-size: 11px; line-height: 18px; text-align: center; color: var(--fg-2); background: var(--bg-side); border: 1px solid var(--rule-2); border-radius: 4px; white-space: nowrap; }
.help-col .help-note { margin: 8px 0 0; font-size: 12px; line-height: 1.4; color: var(--fg-3); }
#help .help-foot { flex: none; margin: 0; padding: 10px 14px 10px 16px; border-top: 1px solid var(--rule); background: var(--bg-side); }
#help .help-foot button.text:hover { background: var(--rule); }
@media (max-width: 600px) {
  .help-body { grid-template-columns: 1fr; }
  #help-col-2 { margin-top: 18px; }
}
button.text { font: inherit; font-size: 13px; color: var(--fg-2); background: transparent; border: 0; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
button.text:hover { color: var(--fg); background: var(--bg-side); }
button.text:focus-visible, button.danger:focus-visible { outline: 2px solid var(--accent); 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. */
.about-box { width: min(560px, 92vw); }
.about-box h2 { text-transform: none; letter-spacing: 0; font-size: 18px; color: var(--fg); margin-bottom: 4px; }
.about-box p { margin: 0 0 14px; font-size: 14px; color: var(--fg-2); }
.about-box dl { grid-template-columns: max-content 1fr; gap: 7px 20px; }
.about-box dt { font-family: inherit; font-size: 13px; color: var(--fg-3); }
.about-box dd { min-width: 0; overflow-wrap: anywhere; }
.about-box dd.path { font-family: var(--mono); font-size: 12.5px; }
.about-box dd.pre { white-space: pre-line; }
.about-box .muted { color: var(--fg-3); }
.about-box a { color: var(--accent); text-decoration: none; }
.about-box a:hover { text-decoration: underline; }
.reset-box { width: min(520px, 92vw); margin: 0; }
.reset-box p { margin: 0 0 12px; font-size: 14px; line-height: 1.5; }
.reset-box label { display: block; font-size: 14px; margin: 0 0 12px; }
.reset-box label[hidden] { display: none; }
.reset-ask input { display: block; width: 100%; margin-top: 6px; font: inherit; font-family: var(--mono); font-size: 15px; padding: 8px 10px; border: 1px solid var(--rule-2); border-radius: 6px; background: var(--bg); color: inherit; outline: none; }
.reset-ask input:focus { border-color: var(--accent); }
.reset-err { color: var(--del-fg); }
.reset-act { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
button.danger { font: inherit; font-size: 13px; font-weight: 550; color: #fff; background: #b3261e; border: 0; padding: 6px 14px; border-radius: 6px; cursor: pointer; }
button.danger:hover { background: #9a1f18; }
button.danger:disabled { opacity: .4; cursor: default; }

/* ---------- print ---------- */
@media print {
  #side, #rail, #chrome, #toasts, #palette, #help, #about, #reset, #queue-bar, pre.code .copy { display: none !important; }
  #app { display: block; }
  #doc { max-width: none; padding: 0; }
  .prose { font-size: 11.5pt; }
  pre.code { white-space: pre-wrap; }
}

/* 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: 12px; color: var(--fg-3); border-radius: 6px; }
.b-empty:hover { background: var(--rule); color: var(--fg-2); }