-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathindex.html
More file actions
2201 lines (2152 loc) · 163 KB
/
Copy pathindex.html
File metadata and controls
2201 lines (2152 loc) · 163 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GitCat — Main Graph Shell</title>
<style>
/* ============================================================
GITCAT · MAIN GRAPH SHELL — "Lamplight / Cozy Terminal"
A warm desk lamp at 1am. Sober pro Git client; the personality
lives in temperature + one sleeping cat (Tama), never in chrome.
Theme-aware: light base, dark via media + [data-theme] override.
============================================================ */
/* Palette is defined once as tokens per theme; the canvas reads --bg/--l0.. live
via getComputedStyle so graph colours always match the CSS. Retuned for clarity:
brighter text, stronger borders, and vivid saturated lane colours. */
:root{
/* light — higher contrast on warm cream */
--bg:#FFFFFF; --panel:#F6F7F9; --elevated:#FFFFFF; --border:#E3E6EB;
--text:#191C22; --muted:#667085; --accent:#5B57D6; --accent2:#2E8B72;
--danger:#D14343; --warning:#B9781C; --success:#3C9A5A;
--l0:#0E9C86; --l1:#D6497A; --l2:#4E5FE0; --l3:#C07E12; --l4:#8B45C4; --l5:#1488BE; --l6:#579A1C;
--fur:#E0A75C; --fur-shade:#C88A3E; --fur-line:#6A4E2D; --ear-in:#ED96A4;
--skin:#F9E2CA; --skin-line:#BE8C63; --eye:#B0680E; --eye-dark:#7E4A0E; --eye-lite:#E7A63E;
--sweater:#F3E6D0; --sweater-line:#C6B088; --mouth:#B85F58;
--ui:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",system-ui,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;
--mono:ui-monospace,"SF Mono","Menlo","Cascadia Code","Consolas",monospace;
--display:"SF Pro Rounded",ui-rounded,"Nunito","Segoe UI Variable",system-ui,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;
--r-card:12px; --r-panel:10px; --r-control:8px; --r-pill:999px; --r-chip:4px;
--topbar-h:46px; --sidebar-w:264px; --detail-w:344px; --ribbon-w:46px; --nook-h:132px; --row-h:26px; --term-h:280px;
--shadow:0 6px 20px rgba(16,18,27,.10), 0 1px 3px rgba(16,18,27,.08);
color-scheme:light;
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
--bg:#0F1116; --panel:#171A21; --elevated:#20242D; --border:#2D323C;
--text:#ECEEF3; --muted:#98A1AE; --accent:#8E88F2; --accent2:#5FD0B4;
--danger:#F06A5B; --warning:#F0973E; --success:#5FC98A;
--l0:#3FD3B6; --l1:#F98CAD; --l2:#8AA0FF; --l3:#F6BA50; --l4:#C79BF0; --l5:#5ED0EA; --l6:#A9DC5F;
--fur:#E1A85C; --fur-shade:#C6883C; --fur-line:#3B2C1B; --ear-in:#DC8B99;
--skin:#F2D4B4; --skin-line:#8C6C4C; --eye:#F5B843; --eye-dark:#A96C14; --eye-lite:#FFD888;
--sweater:#EADCC0; --sweater-line:#B49D74; --mouth:#CB6F65; color-scheme:dark;
}}
:root[data-theme="light"]{
--bg:#FFFFFF; --panel:#F6F7F9; --elevated:#FFFFFF; --border:#E3E6EB;
--text:#191C22; --muted:#667085; --accent:#5B57D6; --accent2:#2E8B72;
--danger:#D14343; --warning:#B9781C; --success:#3C9A5A;
--l0:#0E9C86; --l1:#D6497A; --l2:#4E5FE0; --l3:#C07E12; --l4:#8B45C4; --l5:#1488BE; --l6:#579A1C;
--fur:#E0A75C; --fur-shade:#C88A3E; --fur-line:#6A4E2D; --ear-in:#ED96A4;
--skin:#F9E2CA; --skin-line:#BE8C63; --eye:#B0680E; --eye-dark:#7E4A0E; --eye-lite:#E7A63E;
--sweater:#F3E6D0; --sweater-line:#C6B088; --mouth:#B85F58; color-scheme:light;
}
:root[data-theme="dark"]{
--bg:#0F1116; --panel:#171A21; --elevated:#20242D; --border:#2D323C;
--text:#ECEEF3; --muted:#98A1AE; --accent:#8E88F2; --accent2:#5FD0B4;
--danger:#F06A5B; --warning:#F0973E; --success:#5FC98A;
--l0:#3FD3B6; --l1:#F98CAD; --l2:#8AA0FF; --l3:#F6BA50; --l4:#C79BF0; --l5:#5ED0EA; --l6:#A9DC5F;
--fur:#E1A85C; --fur-shade:#C6883C; --fur-line:#3B2C1B; --ear-in:#DC8B99;
--skin:#F2D4B4; --skin-line:#8C6C4C; --eye:#F5B843; --eye-dark:#A96C14; --eye-lite:#FFD888;
--sweater:#EADCC0; --sweater-line:#B49D74; --mouth:#CB6F65; color-scheme:dark;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--text);font-family:var(--ui);font-size:13px;line-height:1.45;
-webkit-font-smoothing:antialiased;overflow:hidden}
button{font-family:inherit;font-size:inherit;color:inherit;cursor:pointer}
:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:4px}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:8px;border:2px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--muted)}
/* ============================== APP GRID ============================== */
.app{height:100vh;display:grid;
grid-template-columns:var(--sidebar-w) minmax(0,1fr) var(--detail-w);
/* Row 2 (subnav) is `auto`: it collapses to 0 when the submodule-nav strip
renders nothing (SubmoduleNav.svelte's own {#if visible}) — a plain repo
with no submodules shows no strip and loses no vertical space. */
grid-template-rows:var(--topbar-h) auto minmax(0,1fr);
grid-template-areas:"topbar topbar topbar" "subnav subnav subnav" "sidebar graph detail"}
/* graceful degradation for narrower windows so the graph keeps room */
@media (max-width:1180px){:root{--sidebar-w:224px;--detail-w:300px}}
@media (max-width:980px){:root{--sidebar-w:200px;--detail-w:264px}}
/* ============================== TOP BAR ==============================
Three zones, left to right: [brand + repo IDENTITY: repo-pick, branch
pill] -> search -> [repo/global ACTIONS: sync actions, Undo, theme].
Fetch/Pull/Push sit with Undo/theme (all actions), not with repo-pick/
branch-pill (identity/context) — a deliberate split, not "everything
about the repo in one place", per direct feedback that grouping by
identity-vs-action reads clearer than grouping by subject-matter. Every
chip-like control (.repo-pick/.branch-pill/.tb-btn) shares ONE padding
rhythm (6px 10px) and ONE icon system (.tb-ic — see the HTML comment
above the markup for why this replaced the old per-button Unicode
glyphs) so the bar reads as one designed surface instead of several
buttons that each happened to be built at a different time.
Centering search: .tb-left and .tb-right share the SAME flex:1, with
.cmd-hint itself flex:none (fixed width) between them — this puts it at
the bar's TRUE horizontal center regardless of how wide either side's
actual content is. Two earlier attempts both had real bugs: an
absolutely-centered search box could overlap the sync actions whenever
the left side's content was wide enough to cross the bar's true center;
two INDEPENDENT flex:1 spacers (one on each side of search) center it in
whatever space is left over after both sides' own content, which is only
the bar's true center when both sides happen to be equally wide — they
almost never are here, hence the "search feels a bit off-center" report
this replaced. .tb-left/.tb-right's shared flex:1 is the one construction
that's correct for both sides being unequal AND can't overlap (flex items
never overlap). */
.topbar{grid-area:topbar;display:flex;align-items:center;gap:10px;padding:0 12px;
background:var(--panel);border-bottom:1px solid var(--border);position:relative;z-index:40}
/* min-width DELIBERATELY left at its flex default (auto, i.e. "never shrink
below my own content's natural size") — an earlier draft set this to 0 to
let both sides shrink "evenly", which instead let .tb-left get crushed
past what Fetch/Pull/Push actually need, silently clipping Push behind
.sync-actions's own overflow:hidden (there for the segmented-control
corner-rounding, not meant to hide whole buttons). Both sides refusing to
shrink below content means centering degrades gracefully at extreme
window widths (search's own min-width below gives up its space first)
instead of ever silently hiding a real button. */
.tb-left{display:flex;align-items:center;gap:10px;flex:1}
.tb-right{display:flex;align-items:center;gap:10px;flex:1;justify-content:flex-end}
.brand{display:flex;align-items:center;gap:7px;font-family:var(--display);font-weight:700;letter-spacing:.2px;flex:none}
.brand .mark{width:24px;height:24px;flex:none}
.tb-ic{width:14px;height:14px;flex:none}
/* .ico is .tb-ic's sibling for every OTHER icon in the app (buttons/notes/
status glyphs outside the topbar itself) — same reasoning, same visual
language (see the topbar comment above), just a distinct class since these
sit in a lot of contexts .tb-ic's own 14x14/flex:none isn't guaranteed to
fit (e.g. inline within running text, where a small negative
vertical-align keeps the icon from sitting high against the baseline). */
.ico{flex:none;vertical-align:-2px}
/* Repo identity cluster: which repo, which submodule chain, which branch —
grouped with a tighter gap than the bar's own outer 10px so these three
read as one unit, not three peers of Fetch/Pull/Push. */
.tb-cluster{display:flex;align-items:center;gap:6px}
.repo-pick{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--elevated);
border:1px solid var(--border);border-radius:var(--r-control);font-weight:600;min-width:0}
/* Truncates instead of wrapping at narrow window widths — with no rule at
all, a hyphenated repo name (e.g. "viztracer-source") had a natural
soft-wrap point the browser preferred over shrinking the toolbar, so it
wrapped to two lines instead. overflow:hidden zeroes out this flex item's
automatic min-width (and .repo-pick's own min-width:0 above lets that
shrink propagate up to it), so it shrinks and ellipsizes like every other
chip's text does instead. */
.repo-pick .chev{color:var(--muted)}
.repo-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.branch-pill{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:var(--r-pill);
background:color-mix(in srgb,var(--accent) 16%,transparent);border:1px solid color-mix(in srgb,var(--accent) 40%,transparent);
font-weight:600;font-size:12px}
.branch-pill .tb-ic{color:var(--accent)}
/* #pillBranch had no width bound at all — .tb-left/.tb-right deliberately
never shrink below their own content's natural size (see .topbar's own
comment above, added so Fetch/Pull/Push can't get crushed), so a long
branch/feature name either wrapped onto a second line inside the pill or
ballooned the whole topbar wider. Capping just this span (not the whole
.branch-pill, so the ahead/behind .ab badge stays fully visible) keeps the
topbar's own width stable regardless of branch-name length. */
.branch-pill #pillBranch{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}
.branch-pill .ab{font-family:var(--mono);font-size:11px;color:var(--muted)}
.branch-pill .ab b{color:var(--text)}
/* Bigger than before (320 vs. the old 260px basis) per direct feedback;
flex:none (not flex-basis) so .tb-left/.tb-right's own flex:1 is what
negotiates the actual leftover space, not this. min-width keeps it usable
if the window is narrow enough that .tb-left/.tb-right can't both fit
their content at full size. */
.cmd-hint{display:flex;align-items:center;gap:8px;width:320px;min-width:200px;
padding:6px 10px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r-control);color:var(--muted);cursor:text}
.cmd-hint kbd{margin-left:auto;font-family:var(--mono);font-size:11px;padding:1px 6px;border-radius:5px;
background:var(--elevated);border:1px solid var(--border);color:var(--muted)}
/* Fetch/Pull/Push as ONE segmented control (shared frame, internal
dividers) instead of three separate bordered boxes — same underlying
.tb-btn markup/JS hooks (setSyncButtonsBusy/clearSyncButtonsBusy key off
these same #fetchBtn/#pullBtn/#pushBtn ids unchanged), just a shared
container doing the bordering instead of each button doing its own. */
.sync-actions{display:flex;align-items:stretch;background:var(--elevated);
border:1px solid var(--border);border-radius:var(--r-control);overflow:hidden}
.sync-actions .tb-btn{border:none;border-radius:0;background:transparent}
.sync-actions .tb-btn:not(:last-child){border-right:1px solid var(--border)}
.sync-actions .tb-btn:hover{background:var(--panel)}
.tb-btn{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--elevated);
border:1px solid var(--border);border-radius:var(--r-control);font-weight:600;white-space:nowrap;
transition:border-color .12s,background-color .12s}
.tb-btn:hover{border-color:var(--muted)}
.tb-btn:disabled{opacity:.45;cursor:default;pointer-events:none}
.tb-btn .badge{font-family:var(--mono);font-size:11px;background:var(--accent);color:#fff;
border-radius:var(--r-pill);padding:0 6px;font-weight:700}
.tb-btn.undo{border-color:color-mix(in srgb,var(--accent2) 40%,var(--border))}
.tb-btn.undo span{display:flex;align-items:center;gap:6px}
.tb-btn.undo:disabled .badge{background:var(--muted)}
.tb-btn.danger{color:var(--danger);border-color:color-mix(in srgb,var(--danger) 45%,transparent);
background:color-mix(in srgb,var(--danger) 8%,var(--elevated))}
.tb-btn.danger:hover{background:color-mix(in srgb,var(--danger) 16%,var(--elevated))}
.icon-btn{width:32px;height:32px;display:grid;place-items:center;background:var(--elevated);
border:1px solid var(--border);border-radius:var(--r-control);transition:border-color .12s,background-color .12s}
.icon-btn:hover{border-color:var(--muted)}
/* ======================= SUBMODULE NAV STRIP ========================
A slim toolbar under the topbar (src/islands/submodulenav): a 🌳 tree button,
a "root › child › current" breadcrumb, and the current level's sibling tabs.
The grid row is `auto`, so when the island renders nothing this element has no
children and takes zero height. */
.subnav{grid-area:subnav;position:relative}
.subnav-inner{display:flex;align-items:center;gap:8px;height:34px;padding:0 12px;
background:var(--panel);border-bottom:1px solid var(--border);overflow-x:auto;overflow-y:hidden;white-space:nowrap}
.subnav-inner::-webkit-scrollbar{height:0}
.sn-tree-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
border:1px solid var(--border);border-radius:6px;background:var(--elevated);color:var(--muted)}
.sn-tree-btn svg{width:15px;height:15px}
.sn-tree-btn:hover{color:var(--text);border-color:var(--muted)}
.sn-tree-btn.on{color:var(--accent);border-color:var(--accent)}
.sn-crumbs{flex:none;display:flex;align-items:center;gap:3px}
.sn-sep{color:var(--muted);font-size:12px;opacity:.7}
.sn-crumb{display:inline-flex;align-items:center;gap:5px;padding:3px 7px;border:0;border-radius:6px;background:transparent;
color:var(--muted);font-size:12px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sn-crumb:hover:not([disabled]){background:var(--elevated);color:var(--text)}
.sn-crumb.current{color:var(--text);font-weight:600;cursor:default}
/* A faint divider between the breadcrumb (where you are) and the sibling tabs
(where you can go sideways). */
.sn-tabs{flex:none;display:flex;align-items:center;gap:5px;padding-left:9px;margin-left:2px;border-left:1px solid var(--border)}
.sn-tab{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border:1px solid var(--border);border-radius:var(--r-pill);
background:var(--elevated);color:var(--text);font-size:12px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sn-tab:hover:not([disabled]){border-color:var(--muted)}
.sn-tab.current{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent);cursor:default}
.sn-tab[disabled]:not(.current){opacity:.55}
/* Status dot — same palette the sidebar's own submodule rows use. */
.sn-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--muted)}
.sn-dot[data-status="clean"]{background:var(--success)}
.sn-dot[data-status="dirty"]{background:var(--warn,#d8a657)}
.sn-dot[data-status="out-of-date"]{background:var(--accent)}
.sn-dot[data-status="conflicted"]{background:var(--danger)}
.sn-dot[data-status="not-initialized"],.sn-dot[data-status="removed"],.sn-dot[data-status="unreadable"]{background:var(--muted)}
/* Full-tree popover, anchored under the 🌳 button. */
.sn-tree-backdrop{position:fixed;inset:0;z-index:70}
.sn-tree-pop{position:absolute;z-index:71;left:8px;top:34px;min-width:230px;max-width:min(420px,90vw);max-height:60vh;overflow:auto;
background:var(--panel);border:1px solid var(--border);border-radius:var(--r-panel);box-shadow:0 12px 34px rgba(0,0,0,.4);padding:5px}
.sn-tree-head{padding:4px 8px 6px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.sn-tree-empty{display:flex;align-items:center;gap:8px;padding:8px;color:var(--muted);font-size:12px}
.sn-node{display:flex;align-items:center;gap:7px;width:100%;padding:5px 8px;border:0;border-radius:6px;background:transparent;
color:var(--text);font-size:12.5px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sn-node:hover:not([disabled]){background:var(--elevated)}
.sn-node.current{background:color-mix(in srgb,var(--accent) 14%,transparent);cursor:default}
.sn-node.disabled{opacity:.5;cursor:default}
.sn-node-ic{flex:none;width:14px;height:14px;color:var(--muted)}
.sn-node-name{overflow:hidden;text-overflow:ellipsis}
.sn-here{margin-left:auto;padding-left:10px;font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:.03em}
/* ============================== SIDEBAR ============================== */
.sidebar{grid-area:sidebar;display:flex;flex-direction:column;min-height:0;position:relative;
background:var(--panel);border-right:1px solid var(--border)}
/* #sidebarRefs (the Sidebar Svelte island's mount point) is a plain wrapper
div, not itself a flex item bounded by .sidebar's height — without this it
grows to fit the full ref list, pushing the Tama nook below it down and
off the fixed bottom-left spot instead of the list scrolling internally.
flex:1 gives it the remaining height under the nook; display:flex column
is what makes .ref-scroll's own flex:1/overflow:auto (below) apply at all. */
#sidebarRefs{display:flex;flex-direction:column;flex:1;min-height:0}
/* Dedicated "no repository open" state — replaces the filter+ref-groups
entirely rather than showing 4-5 groups all reading "0", which read as
broken rather than "nothing to show yet". flex:1 fills the same space
.ref-scroll normally would, so this centers within the actual available
height instead of collapsing to the top. */
.sidebar-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;
flex:1;min-height:0;padding:28px 18px;text-align:center;gap:7px;color:var(--muted)}
.sidebar-empty .ic{font-size:30px;line-height:1;opacity:.75;margin-bottom:2px}
.sidebar-empty .t{font-size:13px;font-weight:700;color:var(--text)}
.sidebar-empty .sub{font-size:11.5px;line-height:1.6;max-width:190px}
.sidebar-empty .btn{margin-top:8px;font-size:12px;padding:7px 12px}
/* .ref-search wraps ONLY the input+icon, own position:relative context —
.mag's top:50% used to resolve against .ref-filter itself, which was fine
back when the input was its only child, but broke once "Show all
branches" started conditionally sharing that box: the icon drifted below
the input whenever the button pushed the container taller. */
.ref-filter{padding:10px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:6px}
.ref-search{position:relative}
.ref-search input{width:100%;padding:7px 10px 7px 30px;background:var(--bg);border:1px solid var(--border);
border-radius:var(--r-control);color:var(--text);font-size:12px}
.ref-search input:focus{outline:none;border-color:var(--accent)}
.ref-search .mag{position:absolute;left:20px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none}
/* "Auto" (persistent branch-visibility mode — always shown, the entry
point to turn it on) + "Show all branches" (only while a filter, auto or
manual, is active — sidebarCtrl.isFiltering) — same small ghost-button
treatment, own row since they sit below .ref-search rather than any
existing button context. */
.ref-filter-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.ref-filter-actions .auto-toggle,.ref-filter-actions .show-all{border:1px solid var(--border);background:transparent;color:var(--muted);
font-size:11px;padding:3px 8px;border-radius:var(--r-pill);cursor:pointer}
.ref-filter-actions .auto-toggle:hover,.ref-filter-actions .show-all:hover{background:var(--elevated);color:var(--text);border-color:var(--muted)}
.ref-filter-actions .auto-toggle.active{background:color-mix(in srgb,var(--accent) 16%,transparent);
border-color:var(--accent);color:var(--accent);font-weight:600}
.ref-scroll{flex:1;min-height:0;overflow:auto;padding:6px}
.ref-group{margin-bottom:2px}
.ref-group>summary{list-style:none;display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:6px;
cursor:pointer;user-select:none;color:var(--muted);font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
.ref-group>summary::-webkit-details-marker{display:none}
.ref-group>summary:hover{background:var(--elevated)}
.ref-group>summary .tw{transition:transform .15s;color:var(--muted)}
.ref-group[open]>summary .tw{transform:rotate(90deg)}
.ref-group>summary .count{margin-left:auto;font-family:var(--mono);font-size:10px;background:var(--elevated);padding:0 6px;border-radius:var(--r-pill)}
/* "Manage remotes…" entry point in the Remote group's own header, next to
its count badge — reuses .ref-item .ref-menu's exact look (same "⋮ opens a
menu/dialog" glyph+size), just always visible (not hover-revealed) since
it's a persistent header affordance, not a per-row action. */
.ref-group>summary .manage-btn{margin-left:4px;border:none;background:transparent;color:var(--muted);
cursor:pointer;font-size:13px;line-height:1;padding:2px 4px;border-radius:4px;flex:none}
.ref-group>summary .manage-btn:hover{background:var(--panel);color:var(--text)}
/* Ref folder tree (buildRefRows in sidebar.svelte.ts). --depth is set inline
per row; folder and leaf rows each indent by the same 12px per level from
their own base, so nesting is legible at a glance. It defaults to 0, which is
what keeps any .ref-item rendered OUTSIDE the tree (the "+ New branch…" row,
the submodule/snapshot sections) at its original flat inset. */
.ref-item,.ref-folder{--depth:0}
.ref-folder{display:flex;align-items:center;gap:6px;cursor:pointer;border-radius:6px;
padding:4px 8px 4px calc(10px + var(--depth) * 12px);color:var(--muted);font-weight:600;font-size:11px;user-select:none}
.ref-folder:hover{background:var(--elevated);color:var(--text)}
.ref-folder .tw{transition:transform .15s;color:var(--muted);transform:rotate(90deg);flex:none}
.ref-folder.collapsed .tw{transform:none}
.ref-folder .rname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ref-folder .count{font-family:var(--mono);font-size:10px;background:var(--elevated);padding:0 6px;border-radius:var(--r-pill);flex:none}
.ref-folder:hover .count{background:var(--panel)}
/* Collapse-all/expand-all on a section summary. `margin-left:4px` to sit beside
the count badge, NOT `auto`: .count above already claims the row's free space
with an auto margin, and a second auto margin would split that space between
them instead — pushing the badge away from the right edge it's aligned to.
`display:flex` centres the lucide icon in the box. */
.ref-group>summary .fold-btn{margin-left:4px;display:flex;align-items:center}
.ref-item{display:flex;align-items:center;gap:8px;padding:5px 8px 5px calc(24px + var(--depth) * 12px);border-radius:6px;cursor:pointer;position:relative;user-select:none}
.ref-item:hover{background:var(--elevated)}
.ref-item.current{background:color-mix(in srgb,var(--accent) 14%,transparent)}
/* Tracks --depth with the row's own padding, so the "you are here" dot stays in
the current branch's gutter instead of the outermost one when it's nested. */
.ref-item.current::before{content:"";position:absolute;left:calc(10px + var(--depth) * 12px);top:50%;transform:translateY(-50%);width:5px;height:5px;border-radius:50%;background:var(--accent)}
.ref-item .rname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Hover tooltip showing a truncated ref name in full (see Sidebar.svelte's
onRefHover). position:fixed so it escapes .ref-scroll/.sidebar overflow and
extends past the sidebar's right edge over the graph. max-width only caps a
pathologically long name (still far wider than the sidebar). */
.rname-tip{position:fixed;z-index:9000;pointer-events:none;max-width:min(340px,80vw);
background:var(--elevated);color:var(--text);border:1px solid var(--border);border-radius:6px;
padding:6px 8px;font-size:12px;display:flex;flex-direction:column;gap:3px;
box-shadow:0 6px 20px rgba(0,0,0,.4)}
.rname-tip .rt-row{display:flex;align-items:center;gap:7px;white-space:nowrap;overflow:hidden}
.rname-tip .rt-row.cur .rt-name{font-weight:700}
.rname-tip .rt-dot{width:8px;height:8px;border-radius:50%;flex:none}
.rname-tip .rt-name{overflow:hidden;text-overflow:ellipsis}
.rname-tip .rt-kind{margin-left:auto;padding-left:10px;font-size:10px;color:var(--muted);flex:none;text-transform:uppercase;letter-spacing:.03em}
.ref-item.current .rname{font-weight:700}
.ref-item .ab{flex:none;white-space:nowrap;font-family:var(--mono);font-size:10px;color:var(--muted)}
.ref-item .ab .up{color:var(--success)} .ref-item .ab .dn{color:var(--danger)}
.ref-item .dot{width:8px;height:8px;border-radius:50%;flex:none}
.ref-list{display:flex;flex-direction:column}
.ref-item .ref-menu{margin-left:auto;opacity:0;border:none;background:transparent;color:var(--muted);cursor:pointer;font-size:15px;line-height:1;padding:0 4px;border-radius:4px;flex:none}
.ref-item:hover .ref-menu{opacity:1}
/* A row's own Enter/Space now jumps rather than opening this menu (see
Sidebar.svelte's onkeydown), so the ⋮ button is the only keyboard route
to Checkout/etc left on the row — a keyboard user tabbing onto it needs
to actually SEE it light up, not just click-hover users. */
.ref-item:focus-within .ref-menu{opacity:1}
.ref-item .ref-menu:hover{background:var(--elevated);color:var(--text)}
.ref-item.current .ref-menu{opacity:.65}
.ref-item .rname.nb{color:var(--accent);font-weight:600}
.ref-item .rname.mut{color:var(--muted)}
/* Branch-visibility checkbox — sits before .rname in local/remote rows,
toggling whether this branch feeds the commit graph's revwalk (see
sidebarCtrl.toggleBranchVisible). Disabled+always-checked for the current
branch (never hideable) — same flex:none sizing as the row's other small
fixed-size children (.dot/.ab). */
.ref-item .rb-check{flex:none;margin:0;cursor:pointer}
.ref-item .rb-check:disabled{cursor:default;opacity:.6}
/* Copy-branch-name — sits right after .rname, hover-revealed like
.ref-menu (same opacity:0/1 idiom), but its OWN click doesn't jump to the
ref's tip like the row's own click does: copy needs a distinct, small
target rather than stealing that gesture. */
.ref-item .copy-name{flex:none;opacity:0;border:none;background:transparent;color:var(--muted);cursor:pointer;
font-size:11px;line-height:1;padding:2px 4px;border-radius:4px}
.ref-item:hover .copy-name{opacity:1}
/* Same keyboard-visibility reasoning as .ref-item:focus-within .ref-menu
above: without this, tabbing onto the copy button leaves the focused
control invisible while its sibling ⋮ lights up instead. */
.ref-item:focus-within .copy-name{opacity:1}
.ref-item .copy-name:hover{background:var(--elevated);color:var(--text)}
.ref-item.new-branch:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}
/* Any row mid-checkout/delete dims and stops accepting clicks — the
sidebar-wide `busy` flag covers every row (not just the one acted on)
since checkout/delete/create-branch all share one in-flight slot. */
.ref-item.busy{opacity:.5;pointer-events:none}
.ref-item .spinner{margin-left:auto;flex:none}
/* The New Branch inline form — name input + "branch from" picker, stacked
(the sidebar is too narrow for them side by side). Its own wrapper class
(not .ref-item) since outside-click-to-cancel needs a stable container
ref that includes BOTH controls — see Sidebar.svelte's onWindowPointerdown. */
.nb-form{display:flex;flex-direction:column;gap:5px;padding:6px 8px 8px 24px}
.nb-form .nb-input{background:transparent;border:none;border-bottom:1px solid var(--accent);
color:var(--text);font:inherit;font-size:12px;padding:2px 0;outline:none}
.nb-form .nb-row{display:flex;align-items:center;gap:6px}
.nb-form .nb-from{font:inherit;font-size:11px;color:var(--muted);background:var(--bg);
border:1px solid var(--border);border-radius:5px;padding:3px 5px;outline:none;max-width:100%;flex:1;min-width:0}
.nb-form .nb-from:focus{border-color:var(--accent);color:var(--text)}
.nb-form.busy .nb-input{opacity:.6}
/* Snapshots — 2-line rows (subject + sha/time) instead of sha+time alone, so
you can actually tell WHAT state each snapshot captured without opening
it. Its own class (not .ref-item) since it needs an internal flex-column
for the two lines rather than .ref-item's single flex-row. */
.snap-item{display:flex;align-items:flex-start;gap:8px;padding:6px 8px 6px 24px;border-radius:6px}
.snap-item:hover{background:var(--elevated)}
.snap-clickable{cursor:pointer}
.snap-clickable:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.snap-item .dot{margin-top:5px;width:8px;height:8px;border-radius:50%;flex:none}
.snap-main{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}
.snap-subject{font-size:12px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.snap-meta{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--muted)}
.snap-sha{font:inherit;font-family:var(--mono);color:var(--muted);background:transparent;border:none;padding:0;cursor:copy}
.snap-sha:hover{color:var(--accent)}
/* Submodules — read-only status rows (no checkout/menu affordance, unlike
.ref-item, which implies "click to act on this ref"), so its own class
mirrors .snap-item's shape (no cursor:pointer, same left-indent/padding)
instead of reusing .ref-item. The status chip reuses .rr-badge's exact
pill shape (color-mix(...16%,transparent) background + solid-color text)
and the same --success/--warning/--danger severity language already used
per-letter for git file statuses (.wd-file/.tree .st: "A"=success,
"M"=warning, "D"=danger) — clean=success (nothing to do), dirty=warning
(uncommitted edits in the submodule's own worktree, same meaning as a
modified file), out-of-date=danger (checked-out commit no longer matches
what the superproject's index tracks — needs `git submodule update`),
not-initialized=muted (never cloned at all — an inactive/absent state,
same muted language as e.g. bisect's culled cells, not an "error" color),
conflicted=danger too — same severity color as out-of-date (both need
direct user intervention before the submodule can be trusted again), but a
distinct, more urgent case: the superproject's own index has an unresolved
merge conflict AT this submodule's gitlink path (two branches pointed it at
different commits), not just a stale-but-otherwise-fine checkout. */
.sub-item{display:flex;align-items:center;gap:8px;padding:5px 8px 5px 24px;border-radius:6px;cursor:pointer}
.sub-item:hover{background:var(--elevated)}
.sub-item.busy{opacity:.5;pointer-events:none}
.sub-item .rname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The row's own "⋮" popover trigger — .ref-menu is otherwise only scoped to
`.ref-item .ref-menu` (branches/tags, which share that class); submodule
rows use a different class, so this mirrors those rules verbatim
(including the focus-within reveal — a submodule row is tabindex="0"
with the same keyboard-only-route-to-the-menu problem as a ref row)
rather than rendering with no fade/hover/focus styling at all. */
.sub-item .ref-menu{margin-left:auto;opacity:0;border:none;background:transparent;color:var(--muted);cursor:pointer;font-size:15px;line-height:1;padding:0 4px;border-radius:4px;flex:none}
.sub-item:hover .ref-menu{opacity:1}
.sub-item:focus-within .ref-menu{opacity:1}
.sub-item .ref-menu:hover{background:var(--elevated);color:var(--text)}
.sub-status{font-size:10px;font-weight:600;padding:1px 7px;border-radius:var(--r-pill);flex:none;white-space:nowrap}
.sub-status[data-status="clean"]{background:color-mix(in srgb,var(--success) 16%,transparent);color:var(--success)}
.sub-status[data-status="dirty"]{background:color-mix(in srgb,var(--warning) 16%,transparent);color:var(--warning)}
.sub-status[data-status="out-of-date"]{background:color-mix(in srgb,var(--danger) 16%,transparent);color:var(--danger)}
.sub-status[data-status="conflicted"]{background:color-mix(in srgb,var(--danger) 16%,transparent);color:var(--danger)}
.sub-status[data-status="not-initialized"]{background:color-mix(in srgb,var(--muted) 16%,transparent);color:var(--muted)}
/* "removed": already staged for removal by submodule_remove (nothing
committed yet) — muted like "not-initialized" (nothing actionable left),
but a visually distinct row via the "removed (uncommitted)" label that
replaces its action buttons entirely (see Sidebar.svelte). */
.sub-status[data-status="removed"]{background:color-mix(in srgb,var(--muted) 16%,transparent);color:var(--muted)}
/* "unreadable": CRASH FIX — this submodule's own resolved git directory, or
something reachable in its nested-submodule subtree at any depth, was
found cyclic (a malformed/maliciously crafted `.git` gitfile pointer) or
unresolvable, so the backend refused to even call submodule_status for it
(see check_submodule_safe_for_status in submodule.rs) rather than risk the
stack-overflow crash that call is otherwise confirmed to produce.
Deliberately warning-colored, NOT muted like "not-initialized"/"removed"
above (this needs the user's attention — it may be a hostile/corrupted
submodule, not merely an inactive/absent one) and NOT success/clean —
its own action buttons are hidden entirely (see Sidebar.svelte), same as
"removed". */
.sub-status[data-status="unreadable"]{background:color-mix(in srgb,var(--warning) 16%,transparent);color:var(--warning)}
.sub-item .spinner{flex:none}
/* Bulk submodule tools header — sits above the per-submodule rows (not
crammed into the section's <summary>, which toggles open/closed on any
click inside it) — same left-indent as .sub-item so it reads as part of
the same list. Two stacked rows: the recursive checkbox applies to every
bulk action below it (Sync all/Update all), so it gets a full-width row
of its own with a real label, and Sync all/Update all get their own row
underneath with room to breathe. */
.sub-head{display:flex;flex-direction:column;gap:6px;padding:2px 8px 6px 24px}
.sub-recursive{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}
.sub-bulk-row{display:flex;align-items:center;gap:8px}
.sub-update-all{flex:1;font-size:11px;font-weight:600;color:var(--accent2);background:transparent;
border:1px solid color-mix(in srgb,var(--accent2) 45%,var(--border));border-radius:var(--r-control);padding:4px 9px}
.sub-update-all:hover:not([disabled]){background:color-mix(in srgb,var(--accent2) 12%,transparent)}
.sub-update-all[disabled]{opacity:.5;cursor:default}
/* Sidebar hover tooltip that can NEVER escape its own row — native `title`
attributes are unstyleable and unclipped, positioned by the browser near
the cursor with no awareness of the app's own sidebar/graph boundary, so
a wide one (a remote branch name, a full timestamp) can render past the
narrow sidebar's edge and sit on top of unrelated text in the graph pane
next to it (confirmed: "Check out origin/…" rendering over commit
messages). left+right both set means this box's width is bounded by its
own parent — never the viewport — so it truncates instead of escaping. */
[data-tip]{position:relative}
[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:24px;right:6px;top:100%;margin-top:2px;
background:var(--elevated);border:1px solid var(--border);border-radius:6px;padding:3px 7px;
font-size:10.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
z-index:20;box-shadow:var(--shadow);pointer-events:none}
/* Branch pill's own detail tooltip (legacy/main.ts's branchPillTip()) —
overrides the shared [data-tip] rule above rather than reusing it as-is:
that one is single-line/nowrap and bounded by ITS OWN PARENT's width
(built for the sidebar's narrow rows, which need to never escape past the
sidebar edge into the graph pane — see that rule's own comment). The
branch pill sits in the topbar with the whole app below it, so there's no
equivalent edge to worry about, and the content here is deliberately
TWO lines (branch name + what "ahead/behind" are relative to) joined by a
real `\n`, which needs white-space:pre-line (not nowrap) to actually wrap. */
.branch-pill[data-tip]:hover::after{left:0;right:auto;top:calc(100% + 4px);white-space:pre-line;
overflow:visible;text-overflow:clip;min-width:100%;width:max-content;max-width:240px;
font-size:11px;font-weight:400;line-height:1.5;padding:6px 9px}
.ref-pop{position:fixed;z-index:300;min-width:150px;background:var(--panel);border:1px solid var(--border);border-radius:var(--r-control);box-shadow:var(--shadow);padding:4px;display:flex;flex-direction:column;gap:2px}
.ref-pop button{font:inherit;text-align:left;background:transparent;border:none;color:var(--text);padding:6px 10px;border-radius:6px;cursor:pointer}
.ref-pop button:hover:not([disabled]){background:var(--elevated)}
.ref-pop button.danger{color:var(--danger)}
.ref-pop button[disabled]{opacity:.4;cursor:default}
.mono{font-family:var(--mono)} .mut{color:var(--muted)}
/* Commit-row context menu (src/islands/commitmenu) — a `.ref-pop`, just wider
(the branch/tag/submodule popovers' own 150px min-width is too narrow for a
sha+subject header) and with ONE new element `.ref-pop` itself doesn't
cover: a small non-interactive header line naming which commit the actions
below apply to. Everything else (buttons, disabled state, the branch/tag
sub-form) reuses `.ref-pop`/`.nb-form`/`.nb-input`/`.nb-row`/`.mut` verbatim. */
.ref-pop.cm-pop{min-width:230px}
.cm-head{padding:4px 10px 7px;margin-bottom:2px;border-bottom:1px solid var(--border);font-size:11px;line-height:1.35}
.cm-head .sha{color:var(--text)}
.cm-head .subject{display:block;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* cherryPick/merge/revert's loading row — .nb-row's own flex/gap styling is
scoped to `.nb-form .nb-row` (it always sits under an input in the New
Branch/Tag forms); this one sits directly in the popover with no input
above it, so it needs its own minimal layout rule rather than silently
rendering unstyled. */
.cm-pending{display:flex;align-items:center;gap:8px;padding:6px 10px}
/* ---------- Tama's lamp-nook (pinned bottom of sidebar) ---------- */
.nook{height:var(--nook-h);flex:none;position:relative;overflow:hidden;border-top:1px solid var(--border);
display:flex;align-items:center;gap:8px;padding:0 12px;transition:background .4s ease;--sa:var(--muted);
background:radial-gradient(120px 96px at 26% 130%,color-mix(in srgb,var(--sa) 26%,transparent),transparent 70%),var(--panel)}
.tstage{--sa:var(--muted)}
.tstage[data-state="hint"],.tstage[data-state="thinking"],.tstage[data-state="curious"],.tstage[data-state="syncing"]{--sa:var(--accent)}
.tstage[data-state="warn"]{--sa:var(--warning)}
.tstage[data-state="danger"]{--sa:var(--danger)}
.tstage[data-state="celebrate"],.tstage[data-state="greeting"]{--sa:var(--success)}
.tstage[data-state="rescue"]{--sa:var(--accent2)}
.tstage[data-state="confused"]{--sa:var(--warning)}
/* Painted portrait (same TAMA_IMG poses the hero/modals/cheer popover use —
see main.ts's TamaMascot.POSE) instead of the old hand-drawn vector cat
this corner used to be stuck on from before Tama's cat-girl pivot.
.sprite-wrap carries the cursor-parallax tilt (JS-set style.transform,
see gaze()); .sprite itself carries the continuous idle breathe/nod
animations — split across two elements because both would otherwise
fight over the same `transform` property. Sized to match
.tama-cheer .tama-pic's already-proven size, aspect-matched to the
portraits' own ~0.746 native ratio so object-fit:contain barely
letterboxes. No frame/ring around it — the portrait's own white
background blends into .nook's panel color with nothing boxing it in. */
.sprite-wrap{position:relative;flex:none;transition:transform .12s ease;will-change:transform}
.sprite{width:78px;height:104px;display:block;object-fit:contain;object-position:top;
filter:drop-shadow(0 3px 8px rgba(0,0,0,.18));
animation:spriteBreathe 4.2s ease-in-out infinite}
/* Pose swap is a real "pop" (shrink+fade out, swap src, overshoot back in)
rather than a flat crossfade — see TamaMascot.set()'s own comment for the
timing this pairs with. Both classes override the base spriteBreathe
animation for their brief duration (a more specific selector wins), same
trick .nod already used. */
.sprite.swap{animation:spriteSwapOut .11s ease forwards}
.sprite.swap-in{animation:spriteSwapIn .24s cubic-bezier(.2,.8,.2,1)}
.sprite.nod{animation:spriteNod .42s ease}
@keyframes spriteBreathe{
0%,100%{transform:translateY(0) rotate(0deg) scale(1)}
30%{transform:translateY(-1.5px) rotate(-.6deg) scale(1.008)}
60%{transform:translateY(-2.6px) rotate(.4deg) scale(1.012)}
85%{transform:translateY(-.8px) rotate(-.2deg) scale(1.004)}}
@keyframes spriteSwapOut{to{opacity:0;transform:scale(.93) translateY(3px)}}
@keyframes spriteSwapIn{from{opacity:0;transform:scale(.93) translateY(3px)}to{opacity:1;transform:scale(1) translateY(0)}}
@keyframes spriteNod{0%,100%{transform:translateY(0) scale(1)}30%{transform:translateY(-4px) scale(1.04)}60%{transform:translateY(1px) scale(.98)}}
/* Autonomous idle "glance" (Tama.scheduleGlance) — a brief automatic tilt
even when the cursor hasn't moved, so the corner still reads as alive
during a still mouse, not just while actively tracking the cursor. */
.sprite-wrap.glance{animation:spriteGlance 1.1s ease}
@keyframes spriteGlance{
0%,100%{transform:translate(0,0) rotate(0deg)}
30%{transform:translate(-3px,-1px) rotate(-2deg)}
55%{transform:translate(2.5px,-.5px) rotate(1.6deg)}
80%{transform:translate(-1px,0) rotate(-.5deg)}}
/* Mood flourishes that don't need new art — a soft pulsing glow for
danger (layered as a second drop-shadow, not a hard ring — see the
nook's own no-frame decision above), a few CSS-only sparkles for
celebrate, drifting "Z"s for sleep. All absolutely positioned against
.sprite-wrap's own relative box. */
.tstage[data-state="danger"] .sprite{animation:spriteBreathe 4.2s ease-in-out infinite,dangerPulse 1.3s ease-in-out infinite}
@keyframes dangerPulse{0%,100%{filter:drop-shadow(0 3px 8px rgba(0,0,0,.18)) drop-shadow(0 0 0 transparent)}
50%{filter:drop-shadow(0 3px 8px rgba(0,0,0,.18)) drop-shadow(0 0 14px var(--sa))}}
.spark{position:absolute;width:6px;height:6px;opacity:0;pointer-events:none;background:var(--success);
clip-path:polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%)}
.tstage[data-state="celebrate"] .spark{animation:sparkPop 1s ease-in-out infinite}
.spark.s1{top:-4px;left:-6px;animation-delay:0s!important}
.spark.s2{top:8px;right:-8px;animation-delay:.3s!important}
.spark.s3{bottom:6px;left:-4px;animation-delay:.6s!important}
@keyframes sparkPop{0%,100%{opacity:0;transform:scale(.3) rotate(0deg)}50%{opacity:1;transform:scale(1) rotate(50deg)}}
.zz{position:absolute;top:-6px;right:-4px;font-family:var(--display);font-weight:700;color:var(--muted);opacity:0;pointer-events:none}
.tstage[data-state="sleep"] .zz{animation:zzDrift 3.6s ease-in-out infinite}
.zz.z1{font-size:11px;animation-delay:0s!important}
.zz.z2{font-size:14px;top:-14px;right:-10px;animation-delay:1.1s!important}
.zz.z3{font-size:17px;top:-24px;right:-16px;animation-delay:2.2s!important}
@keyframes zzDrift{0%{opacity:0;transform:translate(0,0) scale(.7)}20%{opacity:.6}100%{opacity:0;transform:translate(6px,-14px) scale(1.15)}}
.talk{flex:1;min-width:0}
.toast-line{font-family:var(--display);font-weight:600;font-size:12.5px;line-height:1.32;color:var(--sa);min-height:16px;
opacity:0;transform:translateY(3px);transition:opacity .22s,transform .22s;overflow:hidden;
/* Was clamped to 3 lines (49.5px) — shorter than the nook's own built-in
"mutation.destructive" safety copy already needs (66px/4 lines), so the
end of that warning ("...type the ref name to go on.") was silently cut
off with no ellipsis. 5 lines (82.5px) covers every message this file
sends today with margin, and still fits: .talk's telemetry row + margin
is a fixed ~35px, leaving 132px(nook) - 35px = 97px of headroom. */
display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical}
.toast-line.show{opacity:1;transform:none}
.telemetry{display:flex;align-items:center;gap:6px;margin-top:6px;font-family:var(--mono);font-size:10px;color:var(--muted)}
.telemetry .pip{width:6px;height:6px;border-radius:50%;background:var(--success);flex:none;animation:pip 2.6s ease-out infinite}
@keyframes pip{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--success) 55%,transparent)}70%,100%{box-shadow:0 0 0 5px transparent}}
.nook.is-interacting *{animation-play-state:paused!important}
/* hero catgirl in the empty state */
.tama-hero{display:flex;flex-direction:column;align-items:center;gap:13px;padding:22px 18px 8px;text-align:center}
.hero-bubble{position:relative;max-width:262px;background:var(--elevated);border:1px solid var(--border);border-radius:14px;
padding:11px 14px;font-size:12.5px;line-height:1.55;box-shadow:var(--shadow);margin-top:2px}
.hero-bubble::after{content:"";position:absolute;top:-7px;left:50%;transform:translateX(-50%) rotate(45deg);
width:13px;height:13px;background:var(--elevated);border-left:1px solid var(--border);border-top:1px solid var(--border)}
.hero-bubble b{color:var(--accent);font-family:var(--display)}
.hero-bubble .jp{color:var(--accent2);font-weight:700}
/* Serious mode (.tama-off, see the sprite-wrap/tama-pic rule below): the
rounded card + upward-pointing tail (::after) was designed as a speech
bubble FROM Tama's portrait sitting just above it — with that portrait
hidden, the exact same styling reads as a quote pointing at nobody.
Flattens it to plain, unboxed text instead (matching .hero-hint's own
plain style right below it in the DOM) rather than keeping a "bubble"
shape that no longer has anyone to belong to. */
.tama-off .hero-bubble{background:none;border:none;box-shadow:none;padding:0;margin-top:8px}
.tama-off .hero-bubble::after{content:none}
.hero-stat{font-family:var(--mono);font-size:12px;color:var(--muted);margin-top:4px}
.hero-stat b{color:var(--success)} .hero-stat .n{color:var(--accent);font-weight:700}
.hero-hint{font-size:11.5px;color:var(--muted);line-height:1.75;max-width:270px}
/* painted catgirl art (hero, modals, undo cheer) */
.tama-pic{display:block;object-fit:contain;object-position:top}
.tama-hero-img{width:190px;height:auto;filter:drop-shadow(0 8px 16px rgba(0,0,0,.20))}
.modal-tama{flex:none;width:62px;height:80px;line-height:0}
.modal-tama .tama-pic{width:100%;height:100%}
/* Dev-mode indicator — top:52px sits just BELOW .topbar (46px tall) rather
than at true top:0, deliberately: the topbar's own left corner already
has the GitCat brand mark/repo-picker (see .brand above) right up
against the window edge, and this needs to be tellable apart at a
glance, not fought with for the same few pixels. position:fixed (not
part of .app's own grid) so it overlays everything regardless of which
panel is focused/collapsed. Shown only under `vite`/`tauri dev` — see the
`if(!import.meta.env.DEV)` toggle beside it (legacy/main.ts, next to the
existing #perf HUD's own identical dev-only gate) — never ships lit in a
real `tauri build`/`vite build`, which is the whole point: the user runs
a dev build and a packaged prod build side by side and needs an
unmistakable way to tell which window is which. */
.dev-badge{position:fixed;top:52px;left:10px;z-index:500;display:flex;align-items:center;gap:5px;
padding:3px 9px;border-radius:var(--r-pill);pointer-events:none;
background:color-mix(in srgb,var(--warning) 20%,var(--bg));
border:1px solid color-mix(in srgb,var(--warning) 55%,var(--border));
color:var(--warning);font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.4px;
box-shadow:0 4px 12px rgba(20,14,6,.22)}
.dev-badge .dot{width:6px;height:6px;border-radius:50%;background:var(--warning);flex:none;
box-shadow:0 0 0 3px color-mix(in srgb,var(--warning) 25%,transparent)}
.tama-cheer{position:fixed;right:20px;bottom:20px;z-index:320;display:flex;align-items:flex-end;gap:8px;
padding:10px 15px 10px 8px;background:var(--panel);border:1px solid color-mix(in srgb,var(--success) 50%,var(--border));
border-radius:18px;box-shadow:0 10px 30px rgba(20,14,6,.28),0 0 0 1px color-mix(in srgb,var(--success) 22%,transparent);
transform:translateY(24px) scale(.96);opacity:0;pointer-events:none;transition:transform .32s cubic-bezier(.2,.8,.2,1),opacity .3s}
.tama-cheer.show{transform:none;opacity:1}
.tama-cheer .tama-pic{width:78px;height:104px;flex:none}
.tama-cheer .cheer-txt{font-family:var(--display);font-weight:600;font-size:13px;line-height:1.4;max-width:184px;padding-bottom:10px}
.tama-cheer .cheer-txt .jp{color:var(--accent2);font-weight:700}
/* "Serious work" mode (settings.svelte.ts's tamaEnabled, applied via
legacy/main.ts's setTamaEnabled toggling this class on <body>) — hides
every DECORATIVE Tama portrait: the nook's animated sprite (+ its spark/zz
decorations, all children of .sprite-wrap), the Detail empty-state hero
image, every modal header's small portrait, and the undo "cheer" popover's
image. Deliberately does NOT touch .toast-line/.telemetry (the nook's
.talk sibling, untouched here) — that's the app's ONLY inline status/
error-message surface (there's no separate toast system), so this must
never silently remove a message like "Open a repository first" along with
the character. .nook's own flex row (.sprite-wrap + .talk, gap:8px) just
collapses to .talk alone when .sprite-wrap is hidden — no layout fix
needed, same reasoning .sprite-wrap being flex:none already relies on. */
.tama-off .sprite-wrap,.tama-off .tama-pic,.tama-off .tama-hero-img{display:none}
@media (prefers-reduced-motion:reduce){.sprite,.sprite-wrap{animation:none!important;transition:none!important}
.spark,.zz{animation:none!important}
.toast-line{transition:opacity .01s!important;transform:none!important}
.scrim,.scrim.on,.modal,.scrim.on .modal,.cmdk,.cmdk.on,.cmdk-panel,.cmdk.on .cmdk-panel,
.bisect-panel,.bisect-panel.on,.btn,.tb-btn,.icon-btn,.resize-handle,.cmdk-row{transition-duration:.01s!important}}
/* ============================== GRAPH ============================== */
.graph{grid-area:graph;position:relative;min-width:0;background:var(--bg);overflow:hidden;display:flex}
.ribbon{width:var(--ribbon-w);flex:none;border-right:1px solid var(--border);
background:linear-gradient(var(--panel),var(--bg));position:relative;overflow:hidden}
.ribbon .rlabel{writing-mode:vertical-rl;transform:rotate(180deg);position:absolute;left:5px;bottom:10px;
font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);pointer-events:none}
/* Snapshot ticks — sized as a comfortable click target (the ribbon is a plain
evenly-spaced list now; positions come from ribbon.ts). 12px tall × ~28px
wide, matched to RIBBON_TICK_GAP_PX/RIBBON_MIN_TICK_PX so they never overlap. */
.tick{position:absolute;left:9px;right:6px;height:12px;border-radius:3px;cursor:pointer;
background:color-mix(in srgb,var(--accent) 55%,transparent);transition:transform .12s,background .12s}
.tick:hover{background:var(--accent);transform:scaleX(1.12)}
.tick.pulse{animation:tickpulse .6s ease}
@keyframes tickpulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 70%,transparent)}100%{box-shadow:0 0 0 10px transparent}}
.canvas-wrap{position:relative;flex:1;min-width:0;overflow:hidden}
/* Shown for the full duration of openRepo() (legacy/main.ts) — the graph
canvas keeps rendering whatever repo was open before (tick()'s own rAF
loop never stops), so without this the pane looks fully interactive but
is silently frozen on stale data for however long load_graph takes on a
large repo. Sits on top (no pointer-events override, so it also blocks
clicks on that stale, about-to-be-replaced graph) with the exact same
show/hide lifecycle as the topbar .repo-pick button's own inline spinner. */
.graph-loading{position:absolute;inset:0;z-index:8;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;
background:color-mix(in srgb,var(--bg) 85%,transparent);color:var(--muted);font-size:13px}
.graph-loading .spinner{width:26px;height:26px;border-width:3px}
/* Top-right "still loading" pill (see #loadingPill). Absolute in the graph, above
the perf HUD; only visible while a graph stream is in flight, so it never
competes with the (dev-only) fps readout during normal use. */
.loading-pill{position:absolute;top:14px;right:16px;z-index:9;display:flex;align-items:center;gap:9px;
padding:9px 16px 9px 14px;background:var(--elevated);border-radius:var(--r-pill);white-space:nowrap;
border:1px solid color-mix(in srgb,var(--accent) 60%,var(--border));
font-size:13px;font-weight:600;color:var(--text);
box-shadow:0 4px 18px color-mix(in srgb,var(--accent) 28%,transparent),var(--shadow);
opacity:0;transform:translateY(-6px) scale(.96);transition:opacity .18s,transform .18s;pointer-events:none}
.loading-pill.show{opacity:1;transform:none;animation:loadingPulse 1.7s ease-in-out infinite}
@keyframes loadingPulse{0%,100%{box-shadow:0 4px 18px color-mix(in srgb,var(--accent) 26%,transparent),var(--shadow)}
50%{box-shadow:0 6px 26px color-mix(in srgb,var(--accent) 55%,transparent),var(--shadow)}}
.loading-pill .spinner{width:15px;height:15px;border-width:2px;color:var(--accent);opacity:1}
@media (prefers-reduced-motion:reduce){.loading-pill.show{animation:none}}
#cv{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none}
/* Panel-resize drag guide: a thin vertical rule that follows the cursor while a
splitter is being dragged, so ONLY the line moves — the panels and the graph
canvas don't reflow/re-render until the drag is released (see main.ts's
wireResizeHandle). `will-collapse` recolors it when releasing would collapse
the panel. No transition on `left` (it must track the cursor exactly). */
.resize-guide{position:fixed;top:0;bottom:0;width:2px;margin-left:-1px;z-index:9999;pointer-events:none;
opacity:0;transition:opacity .1s ease;background:var(--accent);box-shadow:0 0 8px color-mix(in srgb,var(--accent) 55%,transparent)}
.resize-guide.on{opacity:.9}
.resize-guide.will-collapse{background:var(--warning);box-shadow:0 0 8px color-mix(in srgb,var(--warning) 55%,transparent)}
.perf{position:absolute;top:10px;right:14px;z-index:6;display:flex;align-items:center;gap:8px;
font-family:var(--mono);font-size:11px;color:var(--muted);
background:var(--panel);border:1px solid var(--border);
border-radius:var(--r-control);padding:4px 8px;box-shadow:var(--shadow)}
.perf b{color:var(--success);font-weight:700}
.perf select,.perf button{font:inherit;color:var(--text);background:var(--elevated);border:1px solid var(--border);
border-radius:6px;padding:2px 6px;cursor:pointer}
.perf .sep{width:1px;height:14px;background:var(--border)}
.perf-dev{display:flex;align-items:center;gap:8px}
.delta-readout{position:absolute;top:44px;right:14px;z-index:6;font-family:var(--mono);font-size:11px;color:var(--text);
background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid color-mix(in srgb,var(--accent) 45%,var(--border));
border-radius:var(--r-control);padding:4px 9px;opacity:0;transition:opacity .18s;pointer-events:none}
.delta-readout.show{opacity:1}
.cp-ghost{position:fixed;z-index:200;pointer-events:none;padding:5px 10px;border-radius:var(--r-control);font-size:12px;font-weight:600;
box-shadow:var(--shadow);display:flex;align-items:center;gap:7px;background:var(--elevated);border:1px solid var(--accent)}
.cp-ghost .g-dot{width:10px;height:10px;border-radius:50%;background:var(--accent)}
.cp-ghost.illegal{border-color:var(--danger);color:var(--danger)}
.cp-ghost.illegal .g-dot{background:var(--danger)}
.cp-ghost .reason{font-weight:500;color:var(--muted);font-size:11px}
/* Shift+drag = merge (instead of cherry-pick) — same ghost, accent2-tinted so
the two gestures read as clearly distinct while dragging. */
.cp-ghost.merge{border-color:var(--accent2)}
.cp-ghost.merge .g-dot{background:var(--accent2)}
.cp-ghost.merge.illegal{border-color:var(--danger);color:var(--danger)}
.cp-ghost.merge.illegal .g-dot{background:var(--danger)}
.cp-ghost .spinner{display:none}
.cp-ghost.pending .g-dot{display:none}
.cp-ghost.pending .spinner{display:inline-block}
/* ============================== DETAIL PANEL ============================== */
.detail{grid-area:detail;min-height:0;overflow:auto;background:var(--panel);border-left:1px solid var(--border);position:relative}
/* Drag handles on the sidebar's right edge and the detail panel's left edge —
live-resize the .app grid's --sidebar-w/--detail-w custom properties (see
src/legacy/main.ts's wireResizeHandle); the existing canvas ResizeObserver
already reacts to the resulting .canvas-wrap size change, no extra wiring
needed there. */
.resize-handle{position:absolute;top:0;bottom:0;width:6px;z-index:5;cursor:col-resize;background:transparent;transition:background-color .12s}
.resize-handle:hover,.resize-handle.active{background:color-mix(in srgb,var(--accent2) 45%,transparent)}
.sidebar-handle{right:-3px}
.detail-handle{left:-3px}
.resizing,.resizing *{cursor:col-resize!important;user-select:none!important}
/* Collapse: dragging a resize handle PAST its own normal minimum (see
wireResizeHandle's own doc comment in legacy/main.ts) now keeps shrinking
the panel instead of just clamping flat at that floor — release past the
halfway point to `--rail-w` and it snaps fully collapsed, leaving ONLY
this handle visible, now stretched to fill the whole (rail-width) panel
and showing a chevron — a discoverable, always-visible "click here to
bring it back" affordance instead of the panel just vanishing with no
way to reopen it short of blindly dragging the same 6px sliver again. */
/* Exempt .scrim: a modal overlay (e.g. the Detail island's expanded-diff popup)
is a full-screen position:fixed child of the panel — collapsing the panel via
Focus mode (⌘\) must not display:none it, or the modal never appears. It hides
itself via its own visibility:hidden when not .on, so this is safe when closed. */
.sidebar.collapsed>*:not(.resize-handle):not(.scrim),.detail.collapsed>*:not(.resize-handle):not(.scrim){display:none}
.sidebar.collapsed .resize-handle,.detail.collapsed .resize-handle{
left:0;right:0;width:auto;cursor:pointer;background:var(--elevated);
display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:13px}
.sidebar.collapsed .resize-handle:hover,.detail.collapsed .resize-handle:hover{background:var(--panel);color:var(--text)}
.sidebar.collapsed .resize-handle::after{content:'\203A'}
.detail.collapsed .resize-handle::after{content:'\2039'}
.detail section{padding:12px 14px;border-bottom:1px solid var(--border)}
.detail .empty{color:var(--muted);text-align:center;padding:48px 20px;font-size:12.5px;line-height:1.7}
.d-subject{font-size:17px;font-weight:700;font-family:var(--display);line-height:1.35;color:var(--text);letter-spacing:.1px}
.d-body{color:var(--muted);white-space:pre-wrap;margin-top:6px;font-size:12px}
/* Long commit body: clamp to a handful of lines with a soft fade, revealed by
the Show more/less toggle (see Detail.svelte). */
.d-body.clamped{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:6;overflow:hidden;
-webkit-mask-image:linear-gradient(to bottom,#000 68%,transparent);mask-image:linear-gradient(to bottom,#000 68%,transparent)}
.d-body-toggle{margin-top:4px;font-size:11px;font-weight:600;color:var(--accent);background:transparent;border:0;padding:2px 0;cursor:pointer}
.d-body-toggle:hover{text-decoration:underline}
.id-strip{display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap}
.hash{font-family:var(--mono);font-size:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r-chip);padding:2px 7px;cursor:copy}
.hash:hover{border-color:var(--accent)}
.gpg{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:2px 8px;border-radius:var(--r-pill)}
.gpg.good{color:var(--success);background:color-mix(in srgb,var(--success) 15%,transparent)}
.gpg.none{color:var(--muted);background:var(--bg)}
.gpg.bad{color:var(--danger);background:color-mix(in srgb,var(--danger) 15%,transparent)}
.who-split{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);border-radius:var(--r-panel);overflow:hidden;margin-top:4px}
.who{background:var(--panel);padding:9px 11px}
.who h4{margin:0 0 5px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
.who .nm{font-weight:700;font-size:12.5px}
.who .em{font-family:var(--mono);font-size:10.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis}
.who .dt{font-size:11px;color:var(--muted);margin-top:3px}
.who .dt-abs{font-size:10px;color:var(--muted);opacity:.7;margin-top:1px}
.who.differ{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--warning) 45%,transparent)}
.refs-here{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}
.d-lab{margin:0 0 6px;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
/* Section-label row with right-aligned tree controls (Collapse/Expand-all) —
used by the commit Detail's "Changes" header. */
.d-lab-row{display:flex;align-items:center;gap:8px;margin-bottom:6px}
.tree-ctl{display:flex;align-items:center;gap:4px;margin-left:auto}
.row-chip{font-size:10px;font-family:var(--mono);padding:1px 6px;border-radius:var(--r-chip);border:1px solid var(--border);background:var(--bg);white-space:nowrap}
.row-chip.head{border-color:var(--accent);color:var(--accent)}
.row-chip.tag{border-color:var(--accent2);color:var(--accent2)}
.row-chip.remote{color:var(--muted)}
/* --l5 is one of the commit-graph's own lane colors (a light blue/cyan in
both themes) — reused here rather than a new one-off hardcoded hex so this
chip stays theme-aware like every other .row-chip variant already is. */
.row-chip.wsl{color:var(--l5);border-color:color-mix(in srgb,var(--l5) 40%,transparent)}
.covered{border:1px solid color-mix(in srgb,var(--accent) 30%,var(--border));border-radius:var(--r-panel);padding:9px 11px;
background:color-mix(in srgb,var(--accent) 8%,var(--bg));font-size:11.5px;display:flex;gap:9px;align-items:flex-start;margin-top:10px}
.covered .ck{width:8px;height:8px;border-radius:2px;background:var(--accent);margin-top:3px;flex:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
.covered b{color:var(--accent)}
.diffstat{display:flex;align-items:center;gap:10px;margin-top:2px}
.diffstat .nums{font-family:var(--mono);font-size:11px}
.diffstat .nums .add{color:var(--success)} .diffstat .nums .del{color:var(--danger)}
.stat-bar{flex:1;height:8px;border-radius:var(--r-pill);overflow:hidden;display:flex;background:var(--bg);border:1px solid var(--border)}
.stat-bar i{height:100%} .stat-bar .a{background:var(--success)} .stat-bar .d{background:var(--danger)}
.tree{font-size:12px;margin-top:10px}
.tree .dir>summary{list-style:none;display:flex;align-items:center;gap:5px;padding:3px 4px;border-radius:5px;cursor:pointer}
.tree .dir>summary::-webkit-details-marker{display:none}
.tree .dir>summary:hover{background:var(--elevated)}
.tree .dir>summary .tw{transition:transform .12s;color:var(--muted);font-size:10px}
.tree .dir[open]>summary .tw{transform:rotate(90deg)}
.tree .file{display:flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border-radius:5px;cursor:pointer}
.tree .file:hover{background:var(--elevated)}
.tree .file.active{background:color-mix(in srgb,var(--accent) 15%,transparent)}
.tree .indent{padding-left:16px}
/* .fname truncates instead of wrapping — without this, a long filename
under deep tree indentation wraps to 2-3 lines, growing .file's own
height; .file's align-items:center then centers the fixed-size .wd-act
icon buttons against that taller row, so they (and their title tooltips)
drift down into the NEXT sibling row instead of staying pinned to this
row's text. flex:none on .st/.badge matches .wd-file's own equivalents
(Workdir.svelte's parallel per-file row) so neither can shrink/wrap either. */
.tree .fname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tree .badge{margin-left:auto;flex:none;white-space:nowrap;font-family:var(--mono);font-size:10px}
.tree .badge .add{color:var(--success)} .tree .badge .del{color:var(--danger)}
.tree .st{flex:none;width:14px;text-align:center;font-family:var(--mono);font-weight:700;font-size:11px}
.tree .st.M{color:var(--warning)} .tree .st.A{color:var(--success)} .tree .st.D{color:var(--danger)}
.diffview{font-family:var(--mono);font-size:11.5px;line-height:1.55;background:var(--bg);border:1px solid var(--border);
border-radius:var(--r-panel);overflow:auto;max-height:320px;margin-top:2px}
/* left:0 (not just top:0) — same reasoning as .diff-line .ln's own sticky
fix below: without it, this header's OWN file-path text scrolls out of
view horizontally just like the line numbers used to, losing "which
file am I looking at" exactly when scrolled right to read a long line. */
.diff-file-h{position:sticky;top:0;left:0;background:var(--elevated);border-bottom:1px solid var(--border);padding:5px 10px;font-weight:600;font-size:11px;color:var(--muted);z-index:1;
display:flex;align-items:center;justify-content:space-between;gap:8px}
.diff-file-h-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
/* .diff-rows wraps every .diff-line as ONE shrink-wrapped block (Detail.svelte/
Workdir.svelte both add this wrapper around their {#each} of rows) —
width:fit-content here grows ONE box to the width of its widest CHILD's
content (the longest line in the whole diff); min-width:100% keeps it at
least filling .diffview's own visible width for a short diff, exactly like
the OLD approach below used to. The difference that actually matters:
.diff-line itself is now width:100% — 100% of THIS wrapper, i.e. the
SAME final width for every row, long or short, not each row computing
its own independent fit-content size. That distinction is BUG FIX,
not styling — see .diff-line .ln's own doc comment for why this
discovery. */
.diff-rows{width:fit-content;min-width:100%}
.diff-line{display:flex;white-space:pre;padding-right:8px;width:100%}
/* position:sticky;left — BUG FIX: scrolling .diffview right to read a long
line used to carry the line-number/+-marker columns away with it (they're
the FIRST cells in the row's own flex layout, same as everything else) —
past even a modest scroll offset, EVERY visible row (not just the long
one) loses its line number/mark AND has its own text start mid-word,
since one shared horizontal scroll position applies to every row
regardless of that row's own length. Pinning these two columns to the
left edge of the viewport (not the row) keeps them visible at any scroll
position, matching how every other diff viewer (VS Code, GitHub, …)
keeps line numbers pinned during horizontal scroll. `background` on each
is required, not decorative: without an OPAQUE backing, the scrolled-
under code text of whatever the sticky cell is now covering would show
through it. Mixed against `--bg` (not `transparent`, unlike the row's own
background below) specifically so the result is fully opaque while still
matching each row TYPE's own visual color exactly.
position:sticky alone was NOT enough, though — empirically found (via
getBoundingClientRect() on a scrolled-right SHORT row's .ln/.sel/.mk)
that they'd render squeezed/overlapping instead of tiled cleanly, but
ONLY on a row shorter than the widest line in the diff. Root cause: each
.diff-line used to independently compute its OWN `width:fit-content;
min-width:100%` — a SHORT row (say 150px of real content) still ends up
only ~313px wide (.diffview's own visible client width, from
min-width:100%), while the LONGEST row in the same diff can be 591px
wide. A sticky element's stuck position is bounded by its OWN row's
right edge, not the scroll container's — so once scrolled past a short
row's 313px-wide box, its sticky .ln/.sel/.mk have nowhere left to sit
within that row and get compressed into whatever sliver remains. Fixed
by giving EVERY row the SAME width instead: `.diff-rows` (wrapping the
whole `{#each}` of rows) shrink-wraps to the widest row's content ONCE,
and each `.diff-line` is `width:100%` of THAT shared wrapper — so every
row, short or long, ends up exactly as wide as the longest one, and
sticky positioning has full room on every row. */
.diff-line .ln{flex:none;width:34px;text-align:right;padding-right:8px;color:var(--muted);opacity:.6;user-select:none;
position:sticky;left:0;background:var(--bg);z-index:1}
/* left:34px is Detail.svelte's own layout (.ln then .mk, nothing between) —
Workdir.svelte's hunk-staging rows additionally have a `.sel` checkbox
BETWEEN them (`.ln`/`.sel`/`.mk`, see the "Hunk/line-level staging"
block below), so `.sel + .mk` there shifts to left:50px (34px + `.sel`'s
own 16px) to sit right after `.sel`'s sticky position instead of
overlapping it. */
.diff-line .mk{flex:none;width:14px;text-align:center;user-select:none;
position:sticky;left:34px;background:var(--bg);z-index:1}
.diff-line .sel+.mk{left:50px}
.diff-line code{white-space:pre}
.diff-line.hunk{background:color-mix(in srgb,var(--accent2) 12%,transparent);color:var(--muted)}
.diff-line.hunk .ln,.diff-line.hunk .sel,.diff-line.hunk .mk{background:color-mix(in srgb,var(--accent2) 12%,var(--bg))}
.diff-line.add{background:color-mix(in srgb,var(--success) 12%,transparent)}
.diff-line.add .ln,.diff-line.add .sel,.diff-line.add .mk{background:color-mix(in srgb,var(--success) 12%,var(--bg))}
.diff-line.add .mk{color:var(--success)}
.diff-line.del{background:color-mix(in srgb,var(--danger) 12%,transparent)}
.diff-line.del .ln,.diff-line.del .sel,.diff-line.del .mk{background:color-mix(in srgb,var(--danger) 12%,var(--bg))}
.diff-line.del .mk{color:var(--danger)}
.tok-key{color:var(--accent);font-weight:600} .tok-str{color:var(--success)} .tok-num{color:var(--accent2)}
.tok-com{color:var(--muted);font-style:italic} .tok-fn{color:var(--l2)} .tok-punc{color:var(--muted)}
/* ============================== EXPANDED DIFF (full-page popup) ==============================
Detail.svelte's embedded .diffview above is capped at max-height:320px —
fine for a glance, cramped for a real changeset. The expand button in
.diff-file-h opens this instead: same .modal.blame-style near-fullscreen
sizing as the Blame/rebase-plan viewers, with the SAME detailCtrl.tree/
diffHeader/diffRows the embedded view already renders (no separate
controller or re-fetch) — a file list down the left (reuses .tree/dirNode
verbatim) so switching files doesn't require closing it first. */
.modal.diffx{width:min(1200px,96vw);height:88vh;display:flex;flex-direction:column}
.diffx-head-main h3{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.modal.diffx .modal-body{flex:1;min-height:0;overflow:hidden;padding:0;display:flex}
/* File panel: a fixed header (Files label + Collapse/Expand-all) over a
scrolling tree, so a changeset with many folders scrolls inside the 280px
column instead of being cut off, and the controls stay pinned. */
.diffx-files{flex:none;width:280px;border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}
.diffx-files-head{display:flex;align-items:center;gap:8px;padding:10px 10px 6px;flex:none}
.diffx-files-scroll{flex:1;min-height:0;overflow:auto;margin-top:0;padding:0 10px 10px}
.diffx-diff{flex:1;min-width:0;min-height:0;max-height:none;border:0;border-radius:0}
/* Drag handle between the file tree and the diff (Detail.svelte). 8px grab zone
with negative margins so it overlaps the .diffx-files border-right instead of
adding a gutter; the accent rule appears on hover/drag/keyboard-focus. */
.diffx-splitter{flex:none;width:8px;margin:0 -4px;cursor:col-resize;position:relative;z-index:5;align-self:stretch}
.diffx-splitter::after{content:"";position:absolute;top:0;bottom:0;left:3px;width:2px;border-radius:2px;background:transparent;transition:background .12s}
.diffx-splitter:hover::after,.diffx-splitter.active::after,.diffx-splitter:focus-visible::after{background:var(--accent)}
.diffx-splitter:focus-visible{outline:none}
/* ============================== WORKDIR (staging + commit + stash) ==============================
Renders in the SAME #detail slot as the sections above, swapped in by
Workdir.svelte whenever the pinned "Uncommitted changes" row is selected
(see legacy/main.ts's selectWorkdir() / the -2 row sentinel). Deliberately
reuses .detail section/.d-lab/.diffview/.diff-line/.hash/.gpg/.spinner/.btn
verbatim (same visual language as the commit detail panel); only the
staging-list rows, commit box, and stash list below are new. */
.wd-msg{width:100%;resize:vertical;min-height:52px;padding:8px 10px;background:var(--bg);
border:1px solid var(--border);border-radius:var(--r-control);color:var(--text);font:inherit;font-size:12.5px}
.wd-msg:focus{outline:none;border-color:var(--accent)}
.wd-msg:disabled{opacity:.6}
.wd-commit-row{display:flex;align-items:center;gap:10px;margin-top:8px}
.wd-amend{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--muted);flex:1;min-width:0}
.wd-commit-row .btn{margin-left:auto;flex:none}
.wd-sec-head{display:flex;align-items:center;gap:8px;margin-bottom:6px}
/* Right-aligned per-section controls (Collapse/Expand-all folder buttons, plus
the unstaged "Stage all"): one flex group pinned right, so a single
margin-left:auto here replaces .wd-stage-all's own (it would otherwise fight
the collapse buttons for the free space and split them apart). */
.wd-sec-actions{display:flex;align-items:center;gap:4px;margin-left:auto}
.wd-sec-actions .wd-stage-all{margin-left:0}
.wd-stage-all{margin-left:auto;font-size:11px;font-weight:600;color:var(--accent2);background:transparent;
border:1px solid color-mix(in srgb,var(--accent2) 45%,var(--border));border-radius:var(--r-control);padding:3px 9px}
.wd-stage-all:hover:not([disabled]){background:color-mix(in srgb,var(--accent2) 12%,transparent)}
.wd-stage-all[disabled]{opacity:.5;cursor:default}
/* Discard all — the danger-toned variant of a section action. */
.wd-discard-all{color:var(--danger);border-color:color-mix(in srgb,var(--danger) 45%,var(--border))}
.wd-discard-all:hover:not([disabled]){background:color-mix(in srgb,var(--danger) 12%,transparent)}
/* Per-file right-click menu (Workdir.svelte). Fixed-position popover + a
full-screen transparent backdrop that catches the dismissing click. */
.wd-rowmenu-backdrop{position:fixed;inset:0;z-index:60}
.wd-rowmenu{position:fixed;z-index:61;min-width:150px;background:var(--panel);border:1px solid var(--border);