@bakapiano/ccsm 0.22.3 → 0.22.5

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.
Files changed (61) hide show
  1. package/CLAUDE.md +538 -538
  2. package/README.md +189 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +139 -139
  5. package/lib/codexSeed.js +183 -183
  6. package/lib/config.js +274 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/localCliSessions.js +519 -519
  10. package/lib/persistedSessions.js +129 -129
  11. package/lib/tunnel.js +621 -621
  12. package/lib/webTerminal.js +225 -225
  13. package/lib/workspace.js +233 -233
  14. package/package.json +57 -57
  15. package/public/css/base.css +99 -99
  16. package/public/css/cards.css +183 -183
  17. package/public/css/feedback.css +504 -504
  18. package/public/css/forms.css +453 -453
  19. package/public/css/layout.css +176 -176
  20. package/public/css/modal.css +190 -190
  21. package/public/css/responsive.css +176 -176
  22. package/public/css/sidebar.css +707 -707
  23. package/public/css/terminals.css +645 -543
  24. package/public/css/tokens.css +81 -81
  25. package/public/css/wco.css +196 -196
  26. package/public/css/widgets.css +2725 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +371 -371
  29. package/public/js/backend.js +149 -149
  30. package/public/js/components/App.js +73 -73
  31. package/public/js/components/DirectoryPicker.js +203 -203
  32. package/public/js/components/EntityFormModal.js +153 -153
  33. package/public/js/components/Modal.js +57 -57
  34. package/public/js/components/OfflineBanner.js +67 -67
  35. package/public/js/components/PageTitleBar.js +13 -13
  36. package/public/js/components/PendingApprovalOverlay.js +128 -128
  37. package/public/js/components/Picker.js +179 -179
  38. package/public/js/components/Popover.js +55 -55
  39. package/public/js/components/RestartOverlay.js +36 -36
  40. package/public/js/components/Sidebar.js +380 -380
  41. package/public/js/components/TerminalInstance.js +159 -22
  42. package/public/js/components/TerminalResizeDebouncer.js +126 -0
  43. package/public/js/components/TerminalView.js +15 -2
  44. package/public/js/components/XtermTerminal.js +74 -15
  45. package/public/js/components/useDragSort.js +67 -67
  46. package/public/js/dialog.js +67 -67
  47. package/public/js/icons.js +212 -212
  48. package/public/js/main.js +296 -296
  49. package/public/js/pages/AboutPage.js +90 -90
  50. package/public/js/pages/ConfigurePage.js +713 -713
  51. package/public/js/pages/LaunchPage.js +421 -421
  52. package/public/js/pages/RemotePage.js +743 -743
  53. package/public/js/pages/SessionsPage.js +199 -80
  54. package/public/js/state.js +335 -335
  55. package/public/manifest.webmanifest +25 -0
  56. package/public/setup/index.html +567 -0
  57. package/scripts/dev.js +149 -149
  58. package/scripts/install.js +153 -153
  59. package/scripts/restart-helper.js +96 -96
  60. package/scripts/upgrade-helper.js +687 -687
  61. package/server.js +1807 -1807
@@ -1,283 +1,283 @@
1
- /* Terminals tab · left rail (active sessions) + right pane (xterm host) */
2
-
3
- /* Terminal-chrome palette. The xterm canvas itself is painted from a JS
4
- theme object (TerminalView.js); these vars colour everything AROUND it —
5
- the pane backdrop, the tab strip, the empty/displaced states, and the
6
- mobile key bar — so the chrome tracks the canvas. Light defaults here;
7
- dark.css overrides under [data-theme="dark"] with the original dark set.
8
- Keep the two in lockstep with the JS THEME_LIGHT / THEME_DARK objects. */
9
- :root {
10
- /* VSCode Light+ neutral panel grays, to sit seamlessly around the
11
- white VSCode terminal canvas (THEME_LIGHT). */
12
- --term-surface: #ffffff; /* matches THEME_LIGHT.background */
13
- --term-on: #333333;
14
- --term-on-dim: rgba(51, 51, 51, 0.70);
15
- --term-on-faint: rgba(51, 51, 51, 0.45);
16
- --term-heading: #1a1a1a;
17
- --term-prompt: #cd3131; /* VSCode ansiRed */
18
- --term-cta-bg: #2c2c2c;
19
- --term-cta-fg: #ffffff;
20
- --term-cta-bg-hover: #000000;
21
- --term-tabstrip: #f0f0f0;
22
- --term-tab: #e4e4e4;
23
- --term-tab-hover: #d8d8d8;
24
- --term-tab-text: rgba(51, 51, 51, 0.70);
25
- --term-keybar-bg: #f0f0f0;
26
- --term-key-fg: #333333;
27
- --term-key-bg: rgba(0, 0, 0, 0.05);
28
- --term-key-border: rgba(0, 0, 0, 0.14);
29
- --term-key-active-bg: rgba(0, 0, 0, 0.12);
30
- --term-key-active-border: rgba(0, 0, 0, 0.28);
31
- --term-key-hint: rgba(0, 0, 0, 0.5);
32
- --term-pop-bg: #f6f6f6;
33
- --term-pop-border: rgba(0, 0, 0, 0.16);
34
- }
35
-
36
- .terminals-layout {
37
- display: grid;
38
- grid-template-columns: 240px 1fr;
39
- gap: var(--s-4);
40
- /* viewport minus page header + footer + padding; lets xterm fill height */
41
- height: calc(100vh - 220px);
42
- min-height: 480px;
43
- }
44
-
45
- .terminals-rail {
46
- background: var(--bg-elev);
47
- border: 1px solid var(--border);
48
- border-radius: var(--r-md);
49
- padding: var(--s-2);
50
- display: flex;
51
- flex-direction: column;
52
- gap: 2px;
53
- overflow-y: auto;
54
- }
55
- .terminals-rail-head {
56
- display: flex;
57
- justify-content: space-between;
58
- align-items: center;
59
- padding: var(--s-2) var(--s-3);
60
- font-size: 11px;
61
- text-transform: uppercase;
62
- letter-spacing: 0.06em;
63
- color: var(--ink-muted);
64
- border-bottom: 1px solid var(--border-soft);
65
- margin-bottom: var(--s-2);
66
- }
67
-
68
- /* button row showing one terminal in the rail */
69
- .terminal-row {
70
- appearance: none;
71
- background: transparent;
72
- border: 0;
73
- width: 100%;
74
- text-align: left;
75
- padding: 8px 10px;
76
- border-radius: var(--r-sm);
77
- cursor: pointer;
78
- display: grid;
79
- grid-template-columns: 10px 1fr auto auto;
80
- align-items: center;
81
- gap: 8px;
82
- color: var(--ink-mid);
83
- font-family: var(--body);
84
- font-size: 13px;
85
- transition: background .12s ease, color .12s ease;
86
- }
87
- .terminal-row:hover { background: var(--bg); color: var(--ink); }
88
- .terminal-row.is-active {
89
- background: var(--sidebar-active);
90
- color: var(--ink);
91
- }
92
- .terminal-row-title {
93
- white-space: nowrap;
94
- overflow: hidden;
95
- text-overflow: ellipsis;
96
- min-width: 0;
97
- font-weight: 500;
98
- }
99
- .terminal-row-meta {
100
- font-family: var(--mono);
101
- font-size: 10.5px;
102
- color: var(--ink-muted);
103
- font-variant-numeric: tabular-nums;
104
- }
105
- .terminal-row-actions { display: inline-flex; }
106
- .terminal-row .action.tiny.danger {
107
- padding: 1px 7px;
108
- font-size: 11px;
109
- min-width: 0;
110
- }
111
-
112
- /* right pane — full-height xterm host */
113
- .terminals-main {
114
- background: var(--bg);
115
- border: 1px solid var(--border);
116
- border-radius: var(--r-md);
117
- padding: var(--s-3);
118
- overflow: hidden;
119
- min-width: 0;
120
- display: flex;
121
- flex-direction: column;
122
- }
123
- .terminal-host {
124
- flex: 1;
125
- min-height: 0;
126
- width: 100%;
127
- /* IME composition (Chinese/Japanese pinyin) lives in absolutely-positioned
128
- .xterm-helper-textarea + .composition-view at the cursor. xterm 5.5 caps
129
- the view's width to the remaining columns, but as a belt-and-braces guard
130
- we still clip here so any residual right-edge overflow is absorbed rather
131
- than expanding the page (it used to trigger a horizontal scrollbar that
132
- "pushed" the layout). Do NOT touch the textarea/composition-view's own
133
- text properties — xterm relies on their single-line behaviour to keep IME
134
- events firing (forcing pre-wrap / break-all eats compositionupdate events
135
- in Chromium and Chinese input stops working entirely). */
136
- overflow: hidden;
137
- contain: layout;
138
- }
139
- /* IME composition box. While you're typing pinyin — before committing the
140
- Chinese characters — xterm writes the in-progress string into
141
- .composition-view at the cursor. VSCode's terminal shows this box in the
142
- terminal's own colours; we do the same. An earlier version hid it outright
143
- (opacity:0) to dodge a layout push, which is why the typed letters were
144
- invisible — but the host's overflow:hidden + contain:layout above already
145
- absorb any overflow, and xterm caps the box width, so hiding it was both
146
- unnecessary and the bug. The helper textarea is left exactly where xterm
147
- puts it (at the cursor, opacity:0) so the OS IME candidate popup anchors
148
- correctly — we no longer move or restyle it. */
149
- .terminal-host .composition-view {
150
- /* !important to beat xterm.css's own `.xterm .composition-view`
151
- (#000/#FFF, same specificity, loaded after us). var() still re-resolves
152
- per theme, so this tracks light/dark automatically. */
153
- background: var(--term-surface) !important;
154
- color: var(--term-on) !important;
155
- border: 1px solid var(--accent);
156
- border-radius: 3px;
157
- padding: 0 3px;
158
- z-index: 5;
159
- }
160
- /* Don't override xterm's background — its renderer (canvas/WebGL) assumes
161
- an opaque surface and ghosts on scroll if we force transparent. The
162
- theme object in TerminalView.js already paints #faf9f5 to match the
163
- surrounding card. */
164
- .terminal-host .xterm-viewport {
165
- scrollbar-width: thin;
166
- }
167
-
168
- .terminal-empty {
169
- height: 100%;
170
- display: flex;
171
- align-items: center;
172
- justify-content: center;
173
- font-size: 13px;
174
- color: var(--ink-muted);
175
- }
176
-
177
- .terminal-empty-page { width: 100%; }
178
-
179
- /* === v1.0 session pane === */
180
-
181
- .sessions-empty {
182
- display: flex;
183
- align-items: center;
184
- justify-content: center;
185
- min-height: 60vh;
186
- /* Decorative faint hairline grid, only in this empty state — adds
187
- editorial atmosphere without affecting any real content view. */
188
- background-image:
189
- linear-gradient(to right, rgba(216, 212, 198, 0.5) 1px, transparent 1px),
190
- linear-gradient(to bottom, rgba(216, 212, 198, 0.5) 1px, transparent 1px);
191
- background-size: 56px 56px;
192
- background-position: center;
193
- position: relative;
194
- }
195
- .sessions-empty::before,
196
- .sessions-empty::after {
197
- content: "";
198
- position: absolute;
199
- inset: 0;
200
- pointer-events: none;
201
- }
202
- .sessions-empty::before {
203
- background: radial-gradient(ellipse at center, transparent 0%, var(--bg) 75%);
204
- }
205
- .sessions-empty-card {
206
- text-align: center;
207
- padding: var(--s-12) var(--s-10);
208
- background: var(--bg-elev);
209
- border: 1px solid var(--border-soft);
210
- border-radius: 6px;
211
- max-width: 440px;
212
- position: relative;
213
- z-index: 1;
214
- box-shadow: var(--shadow-lg);
215
- }
216
- .sessions-empty-card::before {
217
- content: "· EMPTY ·";
218
- display: block;
219
- font-family: var(--mono);
220
- font-size: 10px;
221
- letter-spacing: 0.28em;
222
- color: var(--ink-faint);
223
- margin-bottom: var(--s-4);
224
- }
225
- .sessions-empty-card h2 {
226
- margin: 0 0 var(--s-3);
227
- font-size: 20px;
228
- font-weight: 600;
229
- letter-spacing: -0.015em;
230
- line-height: 1.2;
231
- color: var(--ink);
232
- }
233
- .sessions-empty-card p {
234
- margin: 0 0 var(--s-5);
235
- color: var(--ink-mid);
236
- font-size: 13.5px;
237
- line-height: 1.55;
238
- }
239
-
240
- .session-pane {
241
- display: flex;
242
- flex-direction: column;
243
- /* Fill the entire content area edge-to-edge — negative horizontal margin
244
- cancels .main's horizontal padding so the terminal touches the window
245
- edges (and the sidebar border) with no surrounding chrome. */
246
- flex: 1;
247
- min-height: 0;
248
- height: 100%;
249
- margin: 0 calc(-1 * var(--s-4));
250
- background: var(--bg-elev);
251
- overflow: hidden;
252
- }
253
-
254
- /* Session tabs · sits between the page-title-bar and the session-pane.
255
- One row of small tab buttons; the active one has a darker bg + accent
256
- underline. Last child is a "+" button that bounces to /launch. The
257
- strip is full-bleed (matches .session-pane horizontal extents). */
258
- .session-tabs {
259
- display: flex;
260
- align-items: stretch;
261
- gap: 0;
262
- height: 30px;
263
- flex-shrink: 0;
264
- padding: 0 2px 0 0;
265
- /* Negative bottom margin cancels the tab-panel's gap to the
266
- session-pane underneath, so the strip sits flush against the
267
- terminal. Negative horizontal margin cancels .main's padding so
268
- the strip is full-bleed like the terminal underneath. */
269
- margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
270
- background: var(--term-tabstrip);
271
- border-bottom: 0;
272
- }
273
- .session-tabs-list {
274
- display: flex;
275
- align-items: stretch;
276
- gap: 0;
277
- flex: 1;
278
- min-width: 0;
279
- overflow: hidden;
280
- }
1
+ /* Terminals tab · left rail (active sessions) + right pane (xterm host) */
2
+
3
+ /* Terminal-chrome palette. The xterm canvas itself is painted from a JS
4
+ theme object (TerminalView.js); these vars colour everything AROUND it —
5
+ the pane backdrop, the tab strip, the empty/displaced states, and the
6
+ mobile key bar — so the chrome tracks the canvas. Light defaults here;
7
+ dark.css overrides under [data-theme="dark"] with the original dark set.
8
+ Keep the two in lockstep with the JS THEME_LIGHT / THEME_DARK objects. */
9
+ :root {
10
+ /* VSCode Light+ neutral panel grays, to sit seamlessly around the
11
+ white VSCode terminal canvas (THEME_LIGHT). */
12
+ --term-surface: #ffffff; /* matches THEME_LIGHT.background */
13
+ --term-on: #333333;
14
+ --term-on-dim: rgba(51, 51, 51, 0.70);
15
+ --term-on-faint: rgba(51, 51, 51, 0.45);
16
+ --term-heading: #1a1a1a;
17
+ --term-prompt: #cd3131; /* VSCode ansiRed */
18
+ --term-cta-bg: #2c2c2c;
19
+ --term-cta-fg: #ffffff;
20
+ --term-cta-bg-hover: #000000;
21
+ --term-tabstrip: #f0f0f0;
22
+ --term-tab: #e4e4e4;
23
+ --term-tab-hover: #d8d8d8;
24
+ --term-tab-text: rgba(51, 51, 51, 0.70);
25
+ --term-keybar-bg: #f0f0f0;
26
+ --term-key-fg: #333333;
27
+ --term-key-bg: rgba(0, 0, 0, 0.05);
28
+ --term-key-border: rgba(0, 0, 0, 0.14);
29
+ --term-key-active-bg: rgba(0, 0, 0, 0.12);
30
+ --term-key-active-border: rgba(0, 0, 0, 0.28);
31
+ --term-key-hint: rgba(0, 0, 0, 0.5);
32
+ --term-pop-bg: #f6f6f6;
33
+ --term-pop-border: rgba(0, 0, 0, 0.16);
34
+ }
35
+
36
+ .terminals-layout {
37
+ display: grid;
38
+ grid-template-columns: 240px 1fr;
39
+ gap: var(--s-4);
40
+ /* viewport minus page header + footer + padding; lets xterm fill height */
41
+ height: calc(100vh - 220px);
42
+ min-height: 480px;
43
+ }
44
+
45
+ .terminals-rail {
46
+ background: var(--bg-elev);
47
+ border: 1px solid var(--border);
48
+ border-radius: var(--r-md);
49
+ padding: var(--s-2);
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 2px;
53
+ overflow-y: auto;
54
+ }
55
+ .terminals-rail-head {
56
+ display: flex;
57
+ justify-content: space-between;
58
+ align-items: center;
59
+ padding: var(--s-2) var(--s-3);
60
+ font-size: 11px;
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.06em;
63
+ color: var(--ink-muted);
64
+ border-bottom: 1px solid var(--border-soft);
65
+ margin-bottom: var(--s-2);
66
+ }
67
+
68
+ /* button row showing one terminal in the rail */
69
+ .terminal-row {
70
+ appearance: none;
71
+ background: transparent;
72
+ border: 0;
73
+ width: 100%;
74
+ text-align: left;
75
+ padding: 8px 10px;
76
+ border-radius: var(--r-sm);
77
+ cursor: pointer;
78
+ display: grid;
79
+ grid-template-columns: 10px 1fr auto auto;
80
+ align-items: center;
81
+ gap: 8px;
82
+ color: var(--ink-mid);
83
+ font-family: var(--body);
84
+ font-size: 13px;
85
+ transition: background .12s ease, color .12s ease;
86
+ }
87
+ .terminal-row:hover { background: var(--bg); color: var(--ink); }
88
+ .terminal-row.is-active {
89
+ background: var(--sidebar-active);
90
+ color: var(--ink);
91
+ }
92
+ .terminal-row-title {
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ min-width: 0;
97
+ font-weight: 500;
98
+ }
99
+ .terminal-row-meta {
100
+ font-family: var(--mono);
101
+ font-size: 10.5px;
102
+ color: var(--ink-muted);
103
+ font-variant-numeric: tabular-nums;
104
+ }
105
+ .terminal-row-actions { display: inline-flex; }
106
+ .terminal-row .action.tiny.danger {
107
+ padding: 1px 7px;
108
+ font-size: 11px;
109
+ min-width: 0;
110
+ }
111
+
112
+ /* right pane — full-height xterm host */
113
+ .terminals-main {
114
+ background: var(--bg);
115
+ border: 1px solid var(--border);
116
+ border-radius: var(--r-md);
117
+ padding: var(--s-3);
118
+ overflow: hidden;
119
+ min-width: 0;
120
+ display: flex;
121
+ flex-direction: column;
122
+ }
123
+ .terminal-host {
124
+ flex: 1;
125
+ min-height: 0;
126
+ width: 100%;
127
+ /* IME composition (Chinese/Japanese pinyin) lives in absolutely-positioned
128
+ .xterm-helper-textarea + .composition-view at the cursor. xterm 5.5 caps
129
+ the view's width to the remaining columns, but as a belt-and-braces guard
130
+ we still clip here so any residual right-edge overflow is absorbed rather
131
+ than expanding the page (it used to trigger a horizontal scrollbar that
132
+ "pushed" the layout). Do NOT touch the textarea/composition-view's own
133
+ text properties — xterm relies on their single-line behaviour to keep IME
134
+ events firing (forcing pre-wrap / break-all eats compositionupdate events
135
+ in Chromium and Chinese input stops working entirely). */
136
+ overflow: hidden;
137
+ contain: layout;
138
+ }
139
+ /* IME composition box. While you're typing pinyin — before committing the
140
+ Chinese characters — xterm writes the in-progress string into
141
+ .composition-view at the cursor. VSCode's terminal shows this box in the
142
+ terminal's own colours; we do the same. An earlier version hid it outright
143
+ (opacity:0) to dodge a layout push, which is why the typed letters were
144
+ invisible — but the host's overflow:hidden + contain:layout above already
145
+ absorb any overflow, and xterm caps the box width, so hiding it was both
146
+ unnecessary and the bug. The helper textarea is left exactly where xterm
147
+ puts it (at the cursor, opacity:0) so the OS IME candidate popup anchors
148
+ correctly — we no longer move or restyle it. */
149
+ .terminal-host .composition-view {
150
+ /* !important to beat xterm.css's own `.xterm .composition-view`
151
+ (#000/#FFF, same specificity, loaded after us). var() still re-resolves
152
+ per theme, so this tracks light/dark automatically. */
153
+ background: var(--term-surface) !important;
154
+ color: var(--term-on) !important;
155
+ border: 1px solid var(--accent);
156
+ border-radius: 3px;
157
+ padding: 0 3px;
158
+ z-index: 5;
159
+ }
160
+ /* Don't override xterm's background — its renderer (canvas/WebGL) assumes
161
+ an opaque surface and ghosts on scroll if we force transparent. The
162
+ theme object in TerminalView.js already paints #faf9f5 to match the
163
+ surrounding card. */
164
+ .terminal-host .xterm-viewport {
165
+ scrollbar-width: thin;
166
+ }
167
+
168
+ .terminal-empty {
169
+ height: 100%;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ font-size: 13px;
174
+ color: var(--ink-muted);
175
+ }
176
+
177
+ .terminal-empty-page { width: 100%; }
178
+
179
+ /* === v1.0 session pane === */
180
+
181
+ .sessions-empty {
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ min-height: 60vh;
186
+ /* Decorative faint hairline grid, only in this empty state — adds
187
+ editorial atmosphere without affecting any real content view. */
188
+ background-image:
189
+ linear-gradient(to right, rgba(216, 212, 198, 0.5) 1px, transparent 1px),
190
+ linear-gradient(to bottom, rgba(216, 212, 198, 0.5) 1px, transparent 1px);
191
+ background-size: 56px 56px;
192
+ background-position: center;
193
+ position: relative;
194
+ }
195
+ .sessions-empty::before,
196
+ .sessions-empty::after {
197
+ content: "";
198
+ position: absolute;
199
+ inset: 0;
200
+ pointer-events: none;
201
+ }
202
+ .sessions-empty::before {
203
+ background: radial-gradient(ellipse at center, transparent 0%, var(--bg) 75%);
204
+ }
205
+ .sessions-empty-card {
206
+ text-align: center;
207
+ padding: var(--s-12) var(--s-10);
208
+ background: var(--bg-elev);
209
+ border: 1px solid var(--border-soft);
210
+ border-radius: 6px;
211
+ max-width: 440px;
212
+ position: relative;
213
+ z-index: 1;
214
+ box-shadow: var(--shadow-lg);
215
+ }
216
+ .sessions-empty-card::before {
217
+ content: "· EMPTY ·";
218
+ display: block;
219
+ font-family: var(--mono);
220
+ font-size: 10px;
221
+ letter-spacing: 0.28em;
222
+ color: var(--ink-faint);
223
+ margin-bottom: var(--s-4);
224
+ }
225
+ .sessions-empty-card h2 {
226
+ margin: 0 0 var(--s-3);
227
+ font-size: 20px;
228
+ font-weight: 600;
229
+ letter-spacing: -0.015em;
230
+ line-height: 1.2;
231
+ color: var(--ink);
232
+ }
233
+ .sessions-empty-card p {
234
+ margin: 0 0 var(--s-5);
235
+ color: var(--ink-mid);
236
+ font-size: 13.5px;
237
+ line-height: 1.55;
238
+ }
239
+
240
+ .session-pane {
241
+ display: flex;
242
+ flex-direction: column;
243
+ /* Fill the entire content area edge-to-edge — negative horizontal margin
244
+ cancels .main's horizontal padding so the terminal touches the window
245
+ edges (and the sidebar border) with no surrounding chrome. */
246
+ flex: 1;
247
+ min-height: 0;
248
+ height: 100%;
249
+ margin: 0 calc(-1 * var(--s-4));
250
+ background: var(--bg-elev);
251
+ overflow: hidden;
252
+ }
253
+
254
+ /* Session tabs · sits between the page-title-bar and the session-pane.
255
+ One row of small tab buttons; the active one has a darker bg + accent
256
+ underline. Last child is a "+" button that bounces to /launch. The
257
+ strip is full-bleed (matches .session-pane horizontal extents). */
258
+ .session-tabs {
259
+ display: flex;
260
+ align-items: stretch;
261
+ gap: 0;
262
+ height: 30px;
263
+ flex-shrink: 0;
264
+ padding: 0 2px 0 0;
265
+ /* Negative bottom margin cancels the tab-panel's gap to the
266
+ session-pane underneath, so the strip sits flush against the
267
+ terminal. Negative horizontal margin cancels .main's padding so
268
+ the strip is full-bleed like the terminal underneath. */
269
+ margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
270
+ background: var(--term-tabstrip);
271
+ border-bottom: 0;
272
+ }
273
+ .session-tabs-list {
274
+ display: flex;
275
+ align-items: stretch;
276
+ gap: 0;
277
+ flex: 1;
278
+ min-width: 0;
279
+ overflow: hidden;
280
+ }
281
281
  .session-tabs-right {
282
282
  display: flex;
283
283
  align-items: center;
@@ -285,30 +285,31 @@
285
285
  gap: 4px;
286
286
  padding-right: 2px;
287
287
  }
288
- /* Close the gap to the page-title-bar above — only when there IS one.
289
- In standalone PWA the title-bar is display:none and .session-tabs is
290
- the first tab-panel child; a negative margin-top there would push
291
- the strip up over the OS title-bar border. */
292
- .page-title-bar + .session-tabs {
293
- margin-top: calc(-1 * var(--s-4));
294
- }
288
+ /* Close the gap to the page-title-bar above — only when there IS one.
289
+ In standalone PWA the title-bar is display:none and .session-tabs is
290
+ the first tab-panel child; a negative margin-top there would push
291
+ the strip up over the OS title-bar border. */
292
+ .page-title-bar + .session-tabs {
293
+ margin-top: calc(-1 * var(--s-4));
294
+ }
295
295
  .session-tab {
296
296
  appearance: none;
297
297
  background: var(--term-tab);
298
298
  border: 0;
299
299
  border-bottom: 2px solid transparent;
300
300
  margin-bottom: -1px; /* overlap container border-bottom */
301
- padding: 0 10px;
302
301
  display: inline-flex;
303
302
  align-items: center;
304
303
  gap: 6px;
305
304
  font: inherit;
306
305
  font-size: 12px;
307
- color: var(--term-tab-text);
306
+ color: var(--term-tab-text);
308
307
  cursor: pointer;
309
308
  max-width: 200px;
310
309
  min-width: 0;
311
310
  transition: background-color .12s, color .12s;
311
+ user-select: none;
312
+ position: relative;
312
313
  }
313
314
  .session-tab:hover { background: var(--term-tab-hover); color: var(--term-on); }
314
315
  .session-tab.is-active {
@@ -316,6 +317,51 @@
316
317
  color: var(--term-on);
317
318
  border-bottom-color: var(--term-surface);
318
319
  }
320
+ .session-tab:focus-visible {
321
+ outline: 1px solid var(--accent);
322
+ outline-offset: -2px;
323
+ }
324
+ .session-tab[data-dnd-over="true"] {
325
+ box-shadow: inset 2px 0 0 var(--accent);
326
+ }
327
+ .session-tab::before {
328
+ content: "";
329
+ position: absolute;
330
+ left: 3px;
331
+ top: 0;
332
+ bottom: 0;
333
+ width: 3px;
334
+ border-radius: 0;
335
+ background: var(--ink-faint);
336
+ opacity: .55;
337
+ }
338
+ .session-tab.is-running::before {
339
+ background: var(--green);
340
+ opacity: .9;
341
+ }
342
+ .session-tab.is-working::before {
343
+ background: var(--blue, #4a73a5);
344
+ opacity: .95;
345
+ }
346
+ .session-tab.is-stopped::before {
347
+ background: var(--ink-faint);
348
+ opacity: .45;
349
+ }
350
+ .session-tab-main {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ gap: 6px;
354
+ min-width: 0;
355
+ flex: 1 1 auto;
356
+ height: 100%;
357
+ padding: 0 0 0 14px;
358
+ }
359
+ .session-tab-main[draggable="true"] {
360
+ cursor: grab;
361
+ }
362
+ .session-tab-main[draggable="true"]:active {
363
+ cursor: grabbing;
364
+ }
319
365
  .session-tab-icon { display: inline-flex; flex-shrink: 0; }
320
366
  .session-tab-icon svg { width: 14px; height: 14px; }
321
367
  .session-tab-icon img { width: 14px; height: 14px; }
@@ -325,14 +371,41 @@
325
371
  text-overflow: ellipsis;
326
372
  min-width: 0;
327
373
  }
328
- .session-tab-meta { color: var(--term-tab-text); font-size: 11px; }
329
- .session-tab.is-active .session-tab-meta { color: rgba(255, 255, 255, 0.6); }
374
+ .session-tab-close {
375
+ appearance: none;
376
+ border: 0;
377
+ background: transparent;
378
+ color: currentColor;
379
+ display: inline-flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ width: 18px;
383
+ height: 18px;
384
+ margin-right: 5px;
385
+ border-radius: 3px;
386
+ opacity: 0;
387
+ flex: 0 0 auto;
388
+ cursor: pointer;
389
+ }
390
+ .session-tab:hover .session-tab-close,
391
+ .session-tab.is-active .session-tab-close,
392
+ .session-tab:focus-within .session-tab-close {
393
+ opacity: .72;
394
+ }
395
+ .session-tab-close:hover {
396
+ opacity: 1;
397
+ background: rgba(255, 255, 255, 0.12);
398
+ }
399
+ .session-tab-close svg {
400
+ width: 12px;
401
+ height: 12px;
402
+ }
330
403
  .session-tab-add {
331
404
  background: transparent;
332
405
  max-width: none;
333
406
  padding: 0 8px;
334
- color: #fff;
335
- }
407
+ color: #fff;
408
+ }
336
409
  .session-tab-add:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
337
410
  .session-tab-add svg { width: 14px; height: 14px; }
338
411
 
@@ -360,56 +433,56 @@
360
433
  /* Kebab in the page-title-bar (top-right). Compact 24px square so it
361
434
  doesn't dominate the masthead. In WCO mode the title-bar already
362
435
  reserves padding-right for OS controls, so this slides cleanly to
363
- the left of them. */
364
- .session-menu-btn {
365
- appearance: none;
366
- background: transparent;
367
- border: 0;
368
- width: 26px;
369
- height: 26px;
370
- border-radius: 5px;
371
- display: inline-flex;
372
- align-items: center;
373
- justify-content: center;
374
- /* Follow the terminal foreground so the dots read on both the light
375
- (#f0f0f0) and dark (#252526) tab strip. The old hardcoded #fff was
376
- invisible on the light strip. */
377
- color: var(--term-on);
378
- cursor: pointer;
379
- flex-shrink: 0;
380
- transition: background-color .12s, color .12s;
381
- }
382
- /* Neutral-grey hover tint works on either strip colour (darkens the light
383
- one, lightens the dark one) without needing a per-theme override. */
436
+ the left of them. */
437
+ .session-menu-btn {
438
+ appearance: none;
439
+ background: transparent;
440
+ border: 0;
441
+ width: 26px;
442
+ height: 26px;
443
+ border-radius: 5px;
444
+ display: inline-flex;
445
+ align-items: center;
446
+ justify-content: center;
447
+ /* Follow the terminal foreground so the dots read on both the light
448
+ (#f0f0f0) and dark (#252526) tab strip. The old hardcoded #fff was
449
+ invisible on the light strip. */
450
+ color: var(--term-on);
451
+ cursor: pointer;
452
+ flex-shrink: 0;
453
+ transition: background-color .12s, color .12s;
454
+ }
455
+ /* Neutral-grey hover tint works on either strip colour (darkens the light
456
+ one, lightens the dark one) without needing a per-theme override. */
384
457
  .session-menu-btn:hover:not(:disabled) { background: rgba(128, 128, 128, 0.2); color: var(--term-on); }
385
458
  .session-menu-btn:disabled { opacity: .55; cursor: wait; }
386
459
  .session-menu-btn svg { width: 16px; height: 16px; }
387
-
388
- .session-menu {
389
- background: var(--bg-elev);
390
- border: 1px solid var(--border);
391
- border-radius: 6px;
392
- padding: 4px;
393
- box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.08));
394
- display: flex;
395
- flex-direction: column;
396
- gap: 2px;
397
- }
398
- .session-menu-item {
399
- appearance: none;
400
- background: transparent;
401
- border: 0;
402
- padding: 7px 10px;
403
- border-radius: 4px;
404
- display: flex;
405
- align-items: center;
406
- gap: 8px;
407
- font: inherit;
408
- font-size: 13px;
409
- color: var(--ink);
410
- cursor: pointer;
411
- text-align: left;
412
- }
460
+
461
+ .session-menu {
462
+ background: var(--bg-elev);
463
+ border: 1px solid var(--border);
464
+ border-radius: 6px;
465
+ padding: 4px;
466
+ box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.08));
467
+ display: flex;
468
+ flex-direction: column;
469
+ gap: 2px;
470
+ }
471
+ .session-menu-item {
472
+ appearance: none;
473
+ background: transparent;
474
+ border: 0;
475
+ padding: 7px 10px;
476
+ border-radius: 4px;
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 8px;
480
+ font: inherit;
481
+ font-size: 13px;
482
+ color: var(--ink);
483
+ cursor: pointer;
484
+ text-align: left;
485
+ }
413
486
  .session-menu-item:hover { background: var(--bg); }
414
487
  .session-menu-item.danger { color: var(--danger, #b73f3f); }
415
488
  .session-menu-item.danger:hover { background: rgba(183, 63, 63, 0.08); }
@@ -430,220 +503,249 @@
430
503
  }
431
504
 
432
505
  .session-pane-head {
433
- display: flex;
434
- align-items: center;
435
- gap: var(--s-3);
436
- padding: var(--s-3) var(--s-4);
437
- border-bottom: 1px solid var(--border);
438
- background: var(--bg);
439
- flex-wrap: wrap;
440
- }
441
- .session-pane-title {
442
- display: flex;
443
- align-items: center;
444
- gap: var(--s-2);
445
- }
446
- .session-pane-title h2 {
447
- margin: 0;
448
- font-size: 14.5px;
449
- font-weight: 600;
450
- }
451
- .session-pane-meta {
452
- display: flex;
453
- gap: var(--s-3);
454
- align-items: center;
455
- font-size: 11.5px;
456
- flex: 1;
457
- overflow: hidden;
458
- }
459
- .session-pane-meta .mono {
460
- font-family: var(--mono);
461
- color: var(--ink-mid);
462
- white-space: nowrap;
463
- overflow: hidden;
464
- text-overflow: ellipsis;
465
- max-width: 50%;
466
- }
467
- .session-pane-meta .muted {
468
- color: var(--ink-muted);
469
- }
470
- .session-pane-actions {
471
- display: flex;
472
- gap: var(--s-2);
473
- flex-shrink: 0;
474
- }
506
+ display: flex;
507
+ align-items: center;
508
+ gap: var(--s-3);
509
+ padding: var(--s-3) var(--s-4);
510
+ border-bottom: 1px solid var(--border);
511
+ background: var(--bg);
512
+ flex-wrap: wrap;
513
+ }
514
+ .session-pane-title {
515
+ display: flex;
516
+ align-items: center;
517
+ gap: var(--s-2);
518
+ }
519
+ .session-pane-title h2 {
520
+ margin: 0;
521
+ font-size: 14.5px;
522
+ font-weight: 600;
523
+ }
524
+ .session-pane-meta {
525
+ display: flex;
526
+ gap: var(--s-3);
527
+ align-items: center;
528
+ font-size: 11.5px;
529
+ flex: 1;
530
+ overflow: hidden;
531
+ }
532
+ .session-pane-meta .mono {
533
+ font-family: var(--mono);
534
+ color: var(--ink-mid);
535
+ white-space: nowrap;
536
+ overflow: hidden;
537
+ text-overflow: ellipsis;
538
+ max-width: 50%;
539
+ }
540
+ .session-pane-meta .muted {
541
+ color: var(--ink-muted);
542
+ }
543
+ .session-pane-actions {
544
+ display: flex;
545
+ gap: var(--s-2);
546
+ flex-shrink: 0;
547
+ }
475
548
  .session-pane-body {
476
549
  flex: 1;
477
550
  min-height: 0;
478
551
  background: var(--term-surface);
552
+ position: relative;
553
+ overflow: hidden;
479
554
  }
480
- .session-pane-body .terminal-host {
481
- height: 100%;
555
+ .terminal-stack {
556
+ position: absolute;
557
+ inset: 0;
558
+ min-width: 0;
559
+ min-height: 0;
482
560
  }
483
- .session-pane-body .terminal-empty {
484
- background: var(--term-surface);
485
- color: var(--term-on);
561
+ .terminal-layer {
562
+ position: absolute;
563
+ inset: 0;
564
+ min-width: 0;
565
+ min-height: 0;
486
566
  display: flex;
487
567
  flex-direction: column;
488
- align-items: center;
489
- justify-content: center;
490
- gap: var(--s-3);
491
- height: 100%;
492
- font-size: 13px;
493
- }
494
- .session-pane-body .terminal-empty .mono {
495
- color: var(--term-prompt);
496
- }
497
- .session-pane-body .terminal-empty .action.primary {
498
- background: var(--term-cta-bg);
499
- color: var(--term-cta-fg);
500
- border-color: var(--term-cta-bg);
568
+ visibility: hidden;
569
+ pointer-events: none;
570
+ z-index: 0;
501
571
  }
502
- .session-pane-body .terminal-empty .action.primary:hover {
503
- background: var(--term-cta-bg-hover);
504
- border-color: var(--term-cta-bg-hover);
572
+ .terminal-layer.is-active {
573
+ visibility: visible;
574
+ pointer-events: auto;
575
+ z-index: 1;
505
576
  }
506
-
507
- /* Displaced state — shown when the server kicks us off because another
508
- client attached to the same session (latest-wins). Same dark surface
509
- as terminal-empty so the transition from running terminal → displaced
510
- doesn't flash a colour change. */
511
- .terminal-displaced {
512
- background: var(--term-surface);
513
- color: var(--term-on);
514
- display: flex;
515
- align-items: center;
516
- justify-content: center;
577
+ .session-pane-body .terminal-host {
517
578
  height: 100%;
518
- padding: var(--s-5);
519
- }
520
- .terminal-displaced-card {
521
- max-width: 460px;
522
- text-align: center;
523
- display: flex;
524
- flex-direction: column;
525
- gap: var(--s-3);
526
- }
527
- .terminal-displaced-card h2 {
528
- margin: 0;
529
- font-size: 16px;
530
- font-weight: 600;
531
- color: var(--term-heading);
532
- letter-spacing: -0.005em;
533
- }
534
- .terminal-displaced-card p {
535
- margin: 0;
536
- font-size: 13px;
537
- line-height: 1.55;
538
- color: var(--term-on-dim);
539
579
  }
540
- .terminal-displaced-actions {
541
- margin-top: var(--s-2);
542
- display: flex;
543
- justify-content: center;
544
- }
545
- .terminal-displaced-card .action.primary {
546
- background: var(--term-cta-bg);
547
- color: var(--term-cta-fg);
548
- border-color: var(--term-cta-bg);
549
- padding: 9px 20px;
550
- font-size: 13px;
551
- }
552
- .terminal-displaced-card .action.primary:hover {
553
- background: var(--term-cta-bg-hover);
554
- border-color: var(--term-cta-bg-hover);
555
- }
556
- .terminal-displaced-hint {
557
- font-size: 11.5px !important;
558
- color: var(--term-on-faint) !important;
559
- }
560
-
561
- /* ─── Mobile terminal accessory bar (TerminalKeyBar.js) ───────────────
562
- Floats just above the soft keyboard via a JS-set `bottom` offset
563
- (visualViewport keyboard height). Styled against the dark terminal
564
- palette — it visually belongs to the terminal, not the cream chrome —
565
- so it reads as one surface with the xterm canvas above it. */
566
- .term-keybar {
567
- position: fixed;
568
- left: 0;
569
- right: 0;
570
- z-index: 215; /* above the mobile FAB (210) */
571
- background: var(--term-keybar-bg);
572
- border-top: 1px solid var(--term-key-border);
573
- padding: 6px 8px;
574
- touch-action: manipulation; /* kill the 300ms double-tap-zoom delay */
575
- user-select: none;
576
- -webkit-user-select: none;
577
- /* NOT overflow:auto here — that would clip the Ctrl popover (which sits
578
- at bottom:100%, above the bar). The horizontal scroll lives on the
579
- inner .term-keybar-row instead. */
580
- }
581
- /* Inner scroll row — holds the keys; scrolls horizontally if they don't
582
- fit, without clipping the popover that escapes the bar upward. */
583
- .term-keybar-row {
584
- display: flex;
585
- gap: 6px;
586
- align-items: center;
587
- overflow-x: auto;
588
- -webkit-overflow-scrolling: touch;
589
- white-space: nowrap;
590
- }
591
- .term-keybar-row::-webkit-scrollbar { display: none; }
592
-
593
- .tkb-key {
594
- flex: 0 0 auto;
595
- min-width: 42px;
596
- height: 38px;
597
- display: inline-flex;
598
- align-items: center;
599
- justify-content: center;
600
- padding: 0 12px;
601
- font-family: var(--mono);
602
- font-size: 13px;
603
- line-height: 1;
604
- color: var(--term-key-fg);
605
- background: var(--term-key-bg);
606
- border: 1px solid var(--term-key-border);
607
- border-radius: 8px;
608
- touch-action: manipulation;
609
- -webkit-tap-highlight-color: transparent;
610
- }
611
- .tkb-key:active,
612
- .tkb-key.is-active {
613
- background: var(--term-key-active-bg);
614
- border-color: var(--term-key-active-border);
615
- }
616
- .tkb-arrow { padding: 0 10px; }
617
- .tkb-arrow svg { width: 18px; height: 18px; }
618
- /* S-Tab carries a multi-char label — let it size to content. */
619
- .tkb-wide { padding: 0 12px; }
620
- /* The ↵ glyph renders a touch small in the mono stack; bump it so it
621
- matches the arrow icons' optical weight. */
622
- .tkb-glyph { font-size: 17px; line-height: 1; }
623
-
624
- /* Ctrl combos — a wrap grid that pops ABOVE the bar (bottom:100%). */
625
- .term-keybar-pop {
626
- position: absolute;
627
- bottom: 100%;
628
- left: 8px;
629
- right: 8px;
630
- z-index: 1; /* above the key row inside the bar's context */
631
- margin-bottom: 6px;
632
- display: grid;
633
- grid-template-columns: repeat(5, 1fr);
634
- gap: 6px;
635
- padding: 8px;
636
- background: var(--term-pop-bg);
637
- border: 1px solid var(--term-pop-border);
638
- border-radius: 10px;
639
- box-shadow: 0 -8px 24px -8px rgba(0, 0, 0, 0.5);
640
- }
641
- .tkb-combo {
642
- flex-direction: column;
580
+ .terminal-layer .terminal-host {
581
+ flex: 1 1 auto;
582
+ min-height: 0;
643
583
  height: auto;
644
- min-width: 0;
645
- padding: 7px 4px;
646
- gap: 2px;
647
584
  }
648
- .tkb-combo-label { font-family: var(--mono); font-size: 13px; color: var(--term-key-fg); }
649
- .tkb-combo-hint { font-size: 9.5px; color: var(--term-key-hint); letter-spacing: 0.01em; }
585
+ .session-pane-body .terminal-empty {
586
+ background: var(--term-surface);
587
+ color: var(--term-on);
588
+ display: flex;
589
+ flex-direction: column;
590
+ align-items: center;
591
+ justify-content: center;
592
+ gap: var(--s-3);
593
+ height: 100%;
594
+ font-size: 13px;
595
+ }
596
+ .session-pane-body .terminal-empty .mono {
597
+ color: var(--term-prompt);
598
+ }
599
+ .session-pane-body .terminal-empty .action.primary {
600
+ background: var(--term-cta-bg);
601
+ color: var(--term-cta-fg);
602
+ border-color: var(--term-cta-bg);
603
+ }
604
+ .session-pane-body .terminal-empty .action.primary:hover {
605
+ background: var(--term-cta-bg-hover);
606
+ border-color: var(--term-cta-bg-hover);
607
+ }
608
+
609
+ /* Displaced state — shown when the server kicks us off because another
610
+ client attached to the same session (latest-wins). Same dark surface
611
+ as terminal-empty so the transition from running terminal → displaced
612
+ doesn't flash a colour change. */
613
+ .terminal-displaced {
614
+ background: var(--term-surface);
615
+ color: var(--term-on);
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ height: 100%;
620
+ padding: var(--s-5);
621
+ }
622
+ .terminal-displaced-card {
623
+ max-width: 460px;
624
+ text-align: center;
625
+ display: flex;
626
+ flex-direction: column;
627
+ gap: var(--s-3);
628
+ }
629
+ .terminal-displaced-card h2 {
630
+ margin: 0;
631
+ font-size: 16px;
632
+ font-weight: 600;
633
+ color: var(--term-heading);
634
+ letter-spacing: -0.005em;
635
+ }
636
+ .terminal-displaced-card p {
637
+ margin: 0;
638
+ font-size: 13px;
639
+ line-height: 1.55;
640
+ color: var(--term-on-dim);
641
+ }
642
+ .terminal-displaced-actions {
643
+ margin-top: var(--s-2);
644
+ display: flex;
645
+ justify-content: center;
646
+ }
647
+ .terminal-displaced-card .action.primary {
648
+ background: var(--term-cta-bg);
649
+ color: var(--term-cta-fg);
650
+ border-color: var(--term-cta-bg);
651
+ padding: 9px 20px;
652
+ font-size: 13px;
653
+ }
654
+ .terminal-displaced-card .action.primary:hover {
655
+ background: var(--term-cta-bg-hover);
656
+ border-color: var(--term-cta-bg-hover);
657
+ }
658
+ .terminal-displaced-hint {
659
+ font-size: 11.5px !important;
660
+ color: var(--term-on-faint) !important;
661
+ }
662
+
663
+ /* ─── Mobile terminal accessory bar (TerminalKeyBar.js) ───────────────
664
+ Floats just above the soft keyboard via a JS-set `bottom` offset
665
+ (visualViewport keyboard height). Styled against the dark terminal
666
+ palette — it visually belongs to the terminal, not the cream chrome —
667
+ so it reads as one surface with the xterm canvas above it. */
668
+ .term-keybar {
669
+ position: fixed;
670
+ left: 0;
671
+ right: 0;
672
+ z-index: 215; /* above the mobile FAB (210) */
673
+ background: var(--term-keybar-bg);
674
+ border-top: 1px solid var(--term-key-border);
675
+ padding: 6px 8px;
676
+ touch-action: manipulation; /* kill the 300ms double-tap-zoom delay */
677
+ user-select: none;
678
+ -webkit-user-select: none;
679
+ /* NOT overflow:auto here — that would clip the Ctrl popover (which sits
680
+ at bottom:100%, above the bar). The horizontal scroll lives on the
681
+ inner .term-keybar-row instead. */
682
+ }
683
+ /* Inner scroll row — holds the keys; scrolls horizontally if they don't
684
+ fit, without clipping the popover that escapes the bar upward. */
685
+ .term-keybar-row {
686
+ display: flex;
687
+ gap: 6px;
688
+ align-items: center;
689
+ overflow-x: auto;
690
+ -webkit-overflow-scrolling: touch;
691
+ white-space: nowrap;
692
+ }
693
+ .term-keybar-row::-webkit-scrollbar { display: none; }
694
+
695
+ .tkb-key {
696
+ flex: 0 0 auto;
697
+ min-width: 42px;
698
+ height: 38px;
699
+ display: inline-flex;
700
+ align-items: center;
701
+ justify-content: center;
702
+ padding: 0 12px;
703
+ font-family: var(--mono);
704
+ font-size: 13px;
705
+ line-height: 1;
706
+ color: var(--term-key-fg);
707
+ background: var(--term-key-bg);
708
+ border: 1px solid var(--term-key-border);
709
+ border-radius: 8px;
710
+ touch-action: manipulation;
711
+ -webkit-tap-highlight-color: transparent;
712
+ }
713
+ .tkb-key:active,
714
+ .tkb-key.is-active {
715
+ background: var(--term-key-active-bg);
716
+ border-color: var(--term-key-active-border);
717
+ }
718
+ .tkb-arrow { padding: 0 10px; }
719
+ .tkb-arrow svg { width: 18px; height: 18px; }
720
+ /* S-Tab carries a multi-char label — let it size to content. */
721
+ .tkb-wide { padding: 0 12px; }
722
+ /* The ↵ glyph renders a touch small in the mono stack; bump it so it
723
+ matches the arrow icons' optical weight. */
724
+ .tkb-glyph { font-size: 17px; line-height: 1; }
725
+
726
+ /* Ctrl combos — a wrap grid that pops ABOVE the bar (bottom:100%). */
727
+ .term-keybar-pop {
728
+ position: absolute;
729
+ bottom: 100%;
730
+ left: 8px;
731
+ right: 8px;
732
+ z-index: 1; /* above the key row inside the bar's context */
733
+ margin-bottom: 6px;
734
+ display: grid;
735
+ grid-template-columns: repeat(5, 1fr);
736
+ gap: 6px;
737
+ padding: 8px;
738
+ background: var(--term-pop-bg);
739
+ border: 1px solid var(--term-pop-border);
740
+ border-radius: 10px;
741
+ box-shadow: 0 -8px 24px -8px rgba(0, 0, 0, 0.5);
742
+ }
743
+ .tkb-combo {
744
+ flex-direction: column;
745
+ height: auto;
746
+ min-width: 0;
747
+ padding: 7px 4px;
748
+ gap: 2px;
749
+ }
750
+ .tkb-combo-label { font-family: var(--mono); font-size: 13px; color: var(--term-key-fg); }
751
+ .tkb-combo-hint { font-size: 9.5px; color: var(--term-key-hint); letter-spacing: 0.01em; }