@cplieger/web-terminal-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/css/02-app.css ADDED
@@ -0,0 +1,730 @@
1
+ /* ===== Terminal ===== */
2
+ .term {
3
+ position: fixed;
4
+ inset: 0;
5
+ overflow: hidden auto;
6
+ /* The scroll controller (vterm scroll.ts) is the single owner of
7
+ scrollTop: it pins to the bottom only while the user is following,
8
+ and does nothing while they are scrolled up reading. New output
9
+ always appends at the tail (the absolute-index model never inserts
10
+ above the viewport), so native scroll anchoring is safe to leave on
11
+ and keeps the read position steady on the rare insert-above. */
12
+ overflow-anchor: auto;
13
+ font-family: var(--font-mono);
14
+ font-size: 14px;
15
+ line-height: 17px;
16
+ padding: 4px;
17
+ background: var(--bg);
18
+ color: var(--text);
19
+ }
20
+
21
+ .term-row {
22
+ white-space: pre;
23
+ min-height: 17px;
24
+ font-variant-ligatures: none;
25
+ font-feature-settings:
26
+ "liga" 0,
27
+ "calt" 0;
28
+ font-kerning: none;
29
+
30
+ /* Note: do NOT use `contain: layout style` here. iOS Safari has
31
+ paint-suppression bugs in WebKit where layout-contained elements
32
+ receiving frequent replaceChildren() updates can drop paint
33
+ invalidations for inline children whose styling-only changes
34
+ don't trigger layout — most visibly the inverse-styled "cursor"
35
+ span Ink draws inside our rows, which would visually stick at
36
+ its old column on space/arrow key presses until a content-
37
+ changing keystroke forced a repaint. Without `contain`, every
38
+ row update participates in normal paint flow and the cursor
39
+ cell tracks the column on every flush. */
40
+ }
41
+
42
+ .term-row:empty::after {
43
+ content: "\00a0";
44
+ }
45
+
46
+ /* "earlier output trimmed" marker (vterm renderer inserts .term-trim-marker
47
+ as the first row when history older than the buffer holds was evicted —
48
+ client-side cap or a server-reported resume gap). A quiet, centered,
49
+ non-selectable divider so the user knows scrollback doesn't go further up. */
50
+ .term-trim-marker {
51
+ color: color-mix(in oklch, var(--text) 45%, transparent);
52
+ font-size: 0.6875rem;
53
+ text-align: center;
54
+ padding: 2px 0;
55
+ opacity: 0.7;
56
+ user-select: none;
57
+ font-family: var(--font-ui);
58
+ }
59
+
60
+ .term-input {
61
+ /* Keep the input INSIDE the viewport (not at left:-9999px) — iOS
62
+ * Safari is increasingly strict about not opening the virtual
63
+ * keyboard for off-screen inputs, especially during heavy DOM
64
+ * mutations like a streaming agent turn. A 1×1 fully-transparent
65
+ * pinned-bottom input is "visible enough" for iOS without being
66
+ * tappable or perceptible to the user. opacity:0 + size:1px hides
67
+ * it visually; pointer-events:none makes sure stray taps don't
68
+ * land on it (programmatic focus() still works). */
69
+ position: fixed;
70
+ inset-block-end: 0;
71
+ inset-inline-start: 0;
72
+ width: 1px;
73
+ height: 1px;
74
+ opacity: 0;
75
+ pointer-events: none;
76
+ border: 0;
77
+ resize: none;
78
+ outline: none;
79
+ /* iOS auto-zooms on input focus when the input's font-size < 16px.
80
+ * Lock to 16px so the keyboard pop doesn't trigger a viewport zoom
81
+ * (which would also defer the keyboard show animation). */
82
+ font-size: 16px;
83
+ }
84
+
85
+ .term-output {
86
+ position: relative;
87
+ outline: none;
88
+
89
+ /* Display-only surface: native text selection works (the hterm
90
+ pattern), but it is NOT contenteditable and is never focused — the
91
+ textarea owns input, the keyboard, and IME. Keeping the editable
92
+ element off the scroll content is what lets the first touch-drag
93
+ scroll instead of placing a caret (bug 6), and not re-rendering it
94
+ as an editable keeps a selection alive across redraws (bug 1). */
95
+ user-select: text;
96
+ }
97
+
98
+ /* IME composition view: floating overlay positioned at the cursor
99
+ while the user is composing via an IME / autocorrect / dictation.
100
+ composition.ts mirrors the in-progress text here. CSS direction:rtl
101
+ keeps the visual focus on the trailing edge for long compositions
102
+ so the candidate popup tracks what the user is typing.
103
+ Visibility is gated by the `.active` class so empty state doesn't
104
+ render a phantom box on top of the cursor. */
105
+ .composition-view {
106
+ position: absolute;
107
+ pointer-events: none;
108
+ background: var(--surface);
109
+ color: var(--text);
110
+ border: 1px solid var(--border);
111
+ font-family: var(--font-mono);
112
+ font-size: 14px;
113
+ padding: 0 2px;
114
+ white-space: nowrap;
115
+ direction: rtl;
116
+ z-index: 5;
117
+ display: none;
118
+ }
119
+
120
+ .composition-view.active {
121
+ display: block;
122
+ }
123
+
124
+ /* Predicted-cursor overlay (INPUT-01): rendered at the location the
125
+ cursor will be after the in-flight keystrokes are echoed back by
126
+ the server. Hidden when the prediction matches the server's actual
127
+ cursor (predict.ts deactivates on bail and the renderer hides on
128
+ coincidence). Dashed outline + reduced opacity reads as "tentative"
129
+ — distinct from the solid block of the real cursor. */
130
+ .pred-cursor {
131
+ position: absolute;
132
+ pointer-events: none;
133
+ border: 1px dashed var(--text);
134
+ opacity: 0.55;
135
+ display: none;
136
+ z-index: 4;
137
+ box-sizing: border-box;
138
+ }
139
+
140
+ .pred-cursor.visible {
141
+ display: block;
142
+ }
143
+
144
+ /* Connection-state banner: floats top-center with a subtle backdrop
145
+ blur. Hidden until status.ts sets `.visible`. Distinct from the
146
+ `.pill` family because it wants to span across the full top center
147
+ visually rather than dock to a corner. */
148
+ .conn-banner {
149
+ position: fixed;
150
+ bottom: var(--sp-3);
151
+ left: 50%;
152
+ transform: translateX(-50%);
153
+ max-width: calc(100% - 2 * var(--sp-3));
154
+ padding: var(--sp-1) var(--sp-3);
155
+ background: var(--surface);
156
+ backdrop-filter: var(--blur);
157
+ border: 1px solid var(--border);
158
+ border-radius: var(--radius);
159
+ color: var(--text);
160
+ font-family: var(--font-ui);
161
+ font-size: 0.8125rem;
162
+ font-weight: 500;
163
+ text-align: center;
164
+ z-index: 20;
165
+ display: none;
166
+ pointer-events: auto;
167
+ opacity: 0;
168
+ transition:
169
+ opacity var(--dur-standard) var(--ease-standard),
170
+ display var(--dur-standard) allow-discrete;
171
+ }
172
+
173
+ .conn-banner.visible {
174
+ display: block;
175
+ opacity: 1;
176
+
177
+ @starting-style {
178
+ opacity: 0;
179
+ }
180
+ }
181
+
182
+ /* Color coding by notification type */
183
+ .conn-banner[data-state="reconnecting"] {
184
+ border-color: oklch(70% 0.15 60deg / 40%);
185
+ }
186
+
187
+ .conn-banner[data-state="offline"] {
188
+ color: #f55;
189
+ border-color: oklch(60% 0.2 25deg / 50%);
190
+ }
191
+
192
+ .conn-banner[data-state="restarted"] {
193
+ color: #fa5;
194
+ border-color: oklch(70% 0.15 60deg / 40%);
195
+ }
196
+
197
+ .conn-banner[data-state="toast"] {
198
+ border-color: oklch(70% 0.1 160deg / 30%);
199
+ }
200
+
201
+ /* Virtual keyboard awareness: when the keyboard is open the
202
+ visualViewport height shrinks. viewport.ts sets --kb-inset on
203
+ termWrap; we use it here to lift the banner above the keyboard.
204
+ Falls back to 0px when the variable isn't set (desktop). */
205
+ @media (pointer: coarse) {
206
+ .conn-banner {
207
+ bottom: calc(var(--sp-3) + var(--kb-inset, 0px));
208
+ }
209
+ }
210
+
211
+ /* Clickable URLs */
212
+ .term-link {
213
+ color: inherit;
214
+ text-decoration: underline dotted;
215
+ cursor: pointer;
216
+ }
217
+
218
+ .term-link:hover {
219
+ text-decoration: underline solid;
220
+ }
221
+
222
+ /* Inline cursor cell (xterm.js convention): the character at the cursor
223
+ position gets the .term-cursor class. CSS gives it inverted colors;
224
+ the blink is driven by a JS interval that toggles .cursor-blink-off on
225
+ the viewport. JS-driven instead of CSS animation because the cursor
226
+ span is recreated every row update — a CSS animation would restart on
227
+ each recreation and never reach the "off" frame during streaming. */
228
+ .term-cursor,
229
+ .term-cursor-underline,
230
+ .term-cursor-bar {
231
+ box-sizing: border-box;
232
+ display: inline-block;
233
+ height: 17px;
234
+ line-height: 17px;
235
+ vertical-align: top;
236
+ }
237
+
238
+ .term-cursor {
239
+ background: var(--text);
240
+ color: var(--bg);
241
+ }
242
+
243
+ /* Underline cursor (DECSCUSR 3/4) */
244
+ .term-cursor-underline {
245
+ background: transparent;
246
+ color: var(--text);
247
+ border-bottom: 2px solid var(--text);
248
+ }
249
+
250
+ /* Bar cursor (DECSCUSR 5/6) */
251
+ .term-cursor-bar {
252
+ background: transparent;
253
+ color: var(--text);
254
+ border-left: 2px solid var(--text);
255
+ }
256
+
257
+ .cursor-blink-off .term-cursor {
258
+ background: transparent;
259
+ color: var(--text);
260
+ }
261
+
262
+ .term-blink {
263
+ animation: term-blink-anim 1s step-end infinite;
264
+ }
265
+
266
+ @keyframes term-blink-anim {
267
+ 50% {
268
+ opacity: 0;
269
+ }
270
+ }
271
+
272
+ /* When the terminal isn't focused, render the cursor as an outline
273
+ rather than a filled block (xterm.js convention). */
274
+ .term:not(.focus) .term-cursor,
275
+ .term:not(.focus) .term-cursor-underline,
276
+ .term:not(.focus) .term-cursor-bar {
277
+ background: transparent;
278
+ color: var(--text);
279
+ border: none;
280
+
281
+ /* Inset box-shadow stays strictly inside the cell box without
282
+ affecting layout or clipping at the row boundary. */
283
+ box-shadow: inset 0 0 0 1px var(--text);
284
+ animation: none;
285
+ }
286
+
287
+ /* ===== Terminal color classes (basic 16 — themeable) ===== */
288
+ .term-fg-0 {
289
+ color: #000;
290
+ }
291
+ .term-fg-1 {
292
+ color: #a00;
293
+ }
294
+ .term-fg-2 {
295
+ color: #0a0;
296
+ }
297
+ .term-fg-3 {
298
+ color: #a50;
299
+ }
300
+ .term-fg-4 {
301
+ color: #00a;
302
+ }
303
+ .term-fg-5 {
304
+ color: #a0a;
305
+ }
306
+ .term-fg-6 {
307
+ color: #0aa;
308
+ }
309
+ .term-fg-7 {
310
+ color: #aaa;
311
+ }
312
+ .term-fg-8 {
313
+ color: #555;
314
+ }
315
+ .term-fg-9 {
316
+ color: #f55;
317
+ }
318
+ .term-fg-10 {
319
+ color: #5f5;
320
+ }
321
+ .term-fg-11 {
322
+ color: #ff5;
323
+ }
324
+ .term-fg-12 {
325
+ color: #55f;
326
+ }
327
+ .term-fg-13 {
328
+ color: #f5f;
329
+ }
330
+ .term-fg-14 {
331
+ color: #5ff;
332
+ }
333
+ .term-fg-15 {
334
+ color: #fff;
335
+ }
336
+
337
+ .term-bg-0 {
338
+ background: #000;
339
+ }
340
+ .term-bg-1 {
341
+ background: #a00;
342
+ }
343
+ .term-bg-2 {
344
+ background: #0a0;
345
+ }
346
+ .term-bg-3 {
347
+ background: #a50;
348
+ }
349
+ .term-bg-4 {
350
+ background: #00a;
351
+ }
352
+ .term-bg-5 {
353
+ background: #a0a;
354
+ }
355
+ .term-bg-6 {
356
+ background: #0aa;
357
+ }
358
+ .term-bg-7 {
359
+ background: #aaa;
360
+ }
361
+ .term-bg-8 {
362
+ background: #555;
363
+ }
364
+ .term-bg-9 {
365
+ background: #f55;
366
+ }
367
+ .term-bg-10 {
368
+ background: #5f5;
369
+ }
370
+ .term-bg-11 {
371
+ background: #ff5;
372
+ }
373
+ .term-bg-12 {
374
+ background: #55f;
375
+ }
376
+ .term-bg-13 {
377
+ background: #f5f;
378
+ }
379
+ .term-bg-14 {
380
+ background: #5ff;
381
+ }
382
+ .term-bg-15 {
383
+ background: #fff;
384
+ }
385
+
386
+ /* ===== Key toolbar (touch devices only) =====
387
+ Uses interpolate-size: allow-keywords (on :root) to animate
388
+ width/height between a fixed collapsed size and auto (intrinsic).
389
+ Toggle is first in DOM so it's at top-left of the grid — visible
390
+ when the container clips to 1-cell size. Container is right-anchored
391
+ so it grows leftward on open. */
392
+ .key-toolbar {
393
+ display: contents;
394
+ }
395
+
396
+ /* Touch-only toolbar children: the on-screen Esc/Tab/arrows/hamburger
397
+ are useless on desktop where the user has a real keyboard. Hidden by
398
+ default; the @media (pointer: coarse) block below restores them. */
399
+ .kb-key,
400
+ .kb-toggle {
401
+ display: none;
402
+ }
403
+
404
+ /* Desktop: scroll-to-bottom is still genuinely useful for jumping back
405
+ after scrolling up to read history, so it surfaces here as a
406
+ bottom-center pill with a label. .key-toolbar.scrolled-up is
407
+ toggled by app.ts, same as on touch. The display+opacity transition
408
+ uses allow-discrete so the fade-out plays cleanly when the user
409
+ clicks (scroll-to-bottom clears .scrolled-up, reverting display:none
410
+ after the opacity has gone to 0). */
411
+ .kb-scroll-bottom {
412
+ position: fixed;
413
+ left: 50%;
414
+ bottom: calc(var(--safe-bottom, 0px) + var(--sp-3));
415
+ transform: translateX(-50%);
416
+ display: none;
417
+ opacity: 0;
418
+ align-items: center;
419
+ gap: var(--sp-1);
420
+ padding: var(--sp-2) var(--sp-3);
421
+ background: var(--surface);
422
+ backdrop-filter: var(--blur);
423
+ border: 1px solid var(--border);
424
+ border-radius: var(--radius);
425
+ color: var(--text);
426
+ font: inherit;
427
+ font-size: 0.875rem;
428
+ font-weight: 500;
429
+ cursor: pointer;
430
+ z-index: 10;
431
+ transition:
432
+ opacity var(--dur-standard) var(--ease-standard),
433
+ background-color 150ms var(--ease-standard),
434
+ display var(--dur-standard) allow-discrete;
435
+ }
436
+
437
+ .kb-scroll-bottom::after {
438
+ content: "Scroll to bottom";
439
+ }
440
+
441
+ .kb-scroll-bottom svg {
442
+ width: var(--icon);
443
+ height: var(--icon);
444
+ stroke: currentcolor;
445
+ fill: none;
446
+ stroke-width: 2;
447
+ stroke-linecap: round;
448
+ stroke-linejoin: round;
449
+ }
450
+
451
+ .kb-scroll-bottom:hover {
452
+ background: color-mix(in oklch, var(--text) 8%, var(--surface));
453
+ }
454
+
455
+ .kb-scroll-bottom:active {
456
+ background: var(--text-active);
457
+ }
458
+
459
+ .key-toolbar.scrolled-up .kb-scroll-bottom {
460
+ display: inline-flex;
461
+ opacity: 1;
462
+
463
+ @starting-style {
464
+ opacity: 0;
465
+ }
466
+ }
467
+
468
+ @media (pointer: coarse) {
469
+ .key-toolbar {
470
+ position: fixed;
471
+ top: calc(var(--safe-top) + var(--sp-2) + var(--vv-top, 0px));
472
+ right: var(--sp-2);
473
+ z-index: 10;
474
+ display: grid;
475
+ grid-template-columns: repeat(4, var(--touch-target)) var(--touch-target);
476
+ grid-template-rows: var(--touch-target) var(--touch-target);
477
+ place-content: start end;
478
+ gap: var(--sp-1);
479
+ padding: var(--sp-1);
480
+ background: var(--surface);
481
+ backdrop-filter: var(--blur);
482
+ border: 1px solid var(--border);
483
+ border-radius: var(--radius);
484
+ overflow: hidden;
485
+ width: auto;
486
+ height: auto;
487
+ transition:
488
+ width var(--dur-enter) var(--ease-standard),
489
+ height var(--dur-enter) var(--ease-standard);
490
+ }
491
+
492
+ /* Collapsed: fixed size = 1 button + padding + border. Content is
493
+ right-aligned (justify-content:end) so the toggle (col 5) stays at
494
+ the right edge, visible within the clipped area. --pill-outer
495
+ includes the 2px border (box-sizing:border-box); omitting it left
496
+ the content box 2px short, pinning the button top-right with
497
+ unequal bottom/left gaps. */
498
+ .key-toolbar.collapsed {
499
+ width: var(--pill-outer);
500
+ height: var(--pill-outer);
501
+ }
502
+
503
+ /* Collapsed + scrolled up: 2 buttons tall (+2px border). */
504
+ .key-toolbar.collapsed.scrolled-up {
505
+ height: calc(2 * var(--touch-target) + var(--sp-1) + 2 * var(--sp-1) + 2px);
506
+ }
507
+
508
+ /* Grid placement: toggle at col 5 row 1 (top-right). */
509
+ .kb-toggle {
510
+ grid-area: 1 / 5;
511
+ position: relative;
512
+ }
513
+ .kb-r1c1 {
514
+ grid-area: 1 / 1;
515
+ }
516
+ .kb-r1c2 {
517
+ grid-area: 1 / 2;
518
+ }
519
+ .kb-r1c3 {
520
+ grid-area: 1 / 3;
521
+ }
522
+ .kb-r1c4 {
523
+ grid-area: 1 / 4;
524
+ }
525
+ .kb-r2c1 {
526
+ grid-area: 2 / 1;
527
+ }
528
+ .kb-r2c2 {
529
+ grid-area: 2 / 2;
530
+ }
531
+ .kb-r2c3 {
532
+ grid-area: 2 / 3;
533
+ }
534
+ .kb-r2c4 {
535
+ grid-area: 2 / 4;
536
+ }
537
+
538
+ /* Scroll-bottom: col 5 row 2 (below toggle). Resets the desktop
539
+ FAB-specific properties (fixed position, pill padding, surface
540
+ background, label) so the button collapses back to a grid-area
541
+ icon-only key. */
542
+ .key-toolbar .kb-scroll-bottom {
543
+ position: static;
544
+ transform: none;
545
+ bottom: auto;
546
+ left: auto;
547
+ grid-area: 2 / 5;
548
+ display: none;
549
+ opacity: 0;
550
+ padding: 0;
551
+ background: transparent;
552
+ backdrop-filter: none;
553
+ border: none;
554
+ border-radius: var(--radius-inner);
555
+ font-size: inherit;
556
+ /* Touch override drops the desktop label (::after content: "")
557
+ so only the SVG remains. The base rule sets align-items:center
558
+ but inherits justify-content:flex-start, which would push the
559
+ lone icon to the left edge of the touch-target-sized cell —
560
+ same unequal-padding issue as the toolbar toggle. Center it. */
561
+ justify-content: center;
562
+ transition:
563
+ opacity var(--dur-standard) var(--ease-standard),
564
+ display var(--dur-standard) allow-discrete;
565
+ }
566
+
567
+ .kb-scroll-bottom::after {
568
+ /* No label on touch. Remove the box entirely (not just its text) so
569
+ it isn't an empty flex item — combined with the base rule's
570
+ gap:var(--sp-1) it would shove the lone icon half a gap off-centre. */
571
+ content: "";
572
+ display: none;
573
+ }
574
+ .kb-scroll-bottom:hover {
575
+ background: transparent;
576
+ }
577
+
578
+ .key-toolbar.scrolled-up .kb-scroll-bottom {
579
+ display: flex;
580
+ opacity: 1;
581
+
582
+ @starting-style {
583
+ opacity: 0;
584
+ }
585
+ }
586
+
587
+ /* Keys fade in/out during open/close. The container's width/height
588
+ transition handles the background slide; opacity handles the
589
+ individual button appearance. Stagger the fade slightly after
590
+ the container starts growing (delay on open) and before it
591
+ finishes shrinking (no delay on close). */
592
+ .kb-key {
593
+ opacity: 1;
594
+ transition: opacity var(--dur-standard) var(--ease-enter) 0.05s;
595
+ }
596
+
597
+ .key-toolbar.collapsed .kb-key {
598
+ opacity: 0;
599
+ transition: opacity var(--dur-exit) var(--ease-exit);
600
+ }
601
+
602
+ /* Shared button */
603
+ .key-toolbar button {
604
+ width: var(--touch-target);
605
+ height: var(--touch-target);
606
+ background: transparent;
607
+ color: var(--text);
608
+ border: none;
609
+ border-radius: var(--radius-inner);
610
+ font-size: 0.9375rem;
611
+ font-family: var(--font-ui);
612
+ font-weight: 500;
613
+ cursor: pointer;
614
+ touch-action: manipulation;
615
+ user-select: none;
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ }
620
+
621
+ .key-toolbar button:active {
622
+ background: var(--text-active);
623
+ }
624
+
625
+ /* Sticky Ctrl, armed: inverted fill so the toggle state reads clearly
626
+ in the monochrome palette (stays filled while pressed). */
627
+ .kb-key.armed,
628
+ .kb-key.armed:active {
629
+ background: var(--text);
630
+ color: var(--bg);
631
+ }
632
+
633
+ .key-toolbar button svg {
634
+ width: var(--icon);
635
+ height: var(--icon);
636
+ stroke: currentcolor;
637
+ fill: none;
638
+ stroke-width: 2;
639
+ stroke-linecap: round;
640
+ stroke-linejoin: round;
641
+ }
642
+
643
+ /* Toggle icon crossfade. Both icons are stacked absolute so they
644
+ can independently fade + rotate during the open<->close
645
+ transition. inset:0 + margin:auto centers them within the
646
+ button (works because the SVGs carry explicit width/height); a
647
+ bare `position: absolute` would pin them at the top-left, giving
648
+ the toggle visibly unequal left/right padding when collapsed. */
649
+ .kb-toggle .icon-hamburger,
650
+ .kb-toggle .icon-close {
651
+ position: absolute;
652
+ inset: 0;
653
+ margin: auto;
654
+ transition:
655
+ opacity var(--dur-standard) linear,
656
+ transform var(--dur-standard) linear;
657
+ }
658
+
659
+ .kb-toggle .icon-close {
660
+ opacity: 1;
661
+ transform: rotate(0deg);
662
+ }
663
+ .kb-toggle .icon-hamburger {
664
+ opacity: 0;
665
+ transform: rotate(-90deg);
666
+ }
667
+ .key-toolbar.collapsed .kb-toggle .icon-close {
668
+ opacity: 0;
669
+ transform: rotate(90deg);
670
+ }
671
+ .key-toolbar.collapsed .kb-toggle .icon-hamburger {
672
+ opacity: 1;
673
+ transform: rotate(0deg);
674
+ }
675
+
676
+ /* No-transition on first render */
677
+ .key-toolbar.no-transition,
678
+ .key-toolbar.no-transition .kb-scroll-bottom,
679
+ .key-toolbar.no-transition .kb-toggle .icon-hamburger,
680
+ .key-toolbar.no-transition .kb-toggle .icon-close {
681
+ transition: none;
682
+ }
683
+ }
684
+
685
+ /* ===== Context menu ===== */
686
+ .ctx-menu {
687
+ position: fixed;
688
+ display: none;
689
+ flex-direction: column;
690
+ background: var(--surface);
691
+ backdrop-filter: var(--blur);
692
+ border: 1px solid var(--border);
693
+ border-radius: var(--radius);
694
+ padding: var(--sp-1);
695
+ z-index: 30;
696
+ opacity: 0;
697
+ scale: 0.95;
698
+ transition:
699
+ opacity 0.15s ease,
700
+ scale 0.15s ease,
701
+ display 0.15s allow-discrete;
702
+ }
703
+
704
+ .ctx-menu.visible {
705
+ display: flex;
706
+ opacity: 1;
707
+ scale: 1;
708
+
709
+ @starting-style {
710
+ opacity: 0;
711
+ scale: 0.95;
712
+ }
713
+ }
714
+
715
+ .ctx-menu button {
716
+ background: transparent;
717
+ color: var(--text);
718
+ border: none;
719
+ border-radius: var(--radius-inner);
720
+ font-family: var(--font-ui);
721
+ font-size: 0.8125rem;
722
+ font-weight: 500;
723
+ padding: var(--sp-1) var(--sp-3);
724
+ cursor: pointer;
725
+ text-align: left;
726
+ }
727
+
728
+ .ctx-menu button:active {
729
+ background: var(--text-active);
730
+ }