@cplieger/web-terminal-ui 3.0.3 → 3.1.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.
@@ -0,0 +1,310 @@
1
+ /* ===== Tabs: desktop strip + activity vocabulary (tabs feature) =====
2
+ The desktop top-bar tab strip, its per-tab chrome (label, close, add, the
3
+ right-click menu), the surface offset that clears the fixed strip, and the
4
+ shared activity-dot status vocabulary. The mobile tab chrome (switcher bar,
5
+ overview sheet, external key grid) lives in 06-mobile.css; all motion lives in
6
+ 07-animations.css. */
7
+
8
+ .wt-region-top-bar {
9
+ /* On desktop the tab strip docks to the top; the region is transparent, so
10
+ the bar element below owns its own fixed placement. */
11
+ display: contents;
12
+ }
13
+
14
+ .wt-tab-bar {
15
+ position: fixed;
16
+ bottom: var(--safe-bottom);
17
+ left: 0;
18
+ right: 0;
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--sp-1);
22
+ /* Breathing room around the tab area (a roomier bottom bar). */
23
+ padding: var(--sp-2);
24
+ background: var(--surface);
25
+ backdrop-filter: var(--blur);
26
+ border-top: 1px solid var(--border);
27
+ overflow-x: auto;
28
+ z-index: 15;
29
+ }
30
+
31
+ .wt-tab {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: var(--sp-1);
35
+ /* Browser-style tabs: 140px when there is room, shrinking toward a 90px floor
36
+ as the strip fills; past the floor the bar scrolls horizontally
37
+ (.wt-tab-bar has overflow-x: auto). box-sizing keeps the basis a true total. */
38
+ box-sizing: border-box;
39
+ flex: 0 1 140px;
40
+ min-width: 90px;
41
+ padding: var(--sp-2) var(--sp-2);
42
+ /* A transparent border is reserved on every tab so toggling the inactive
43
+ outline (below) never shifts the strip's layout. */
44
+ border: 1px solid transparent;
45
+ border-radius: var(--radius-inner);
46
+ color: color-mix(in oklch, var(--text) 70%, transparent);
47
+ font-family: var(--font-ui);
48
+ font-size: 0.8125rem;
49
+ cursor: pointer;
50
+ white-space: nowrap;
51
+ user-select: none;
52
+ }
53
+
54
+ /* Inactive tabs read as outlined chips (so adjacent tabs stay visually
55
+ separated) and take a subtle fill on hover. */
56
+ .wt-tab:not(.wt-tab-active) {
57
+ border-color: var(--border);
58
+ }
59
+
60
+ .wt-tab:not(.wt-tab-active):hover {
61
+ background: var(--tab-hover-bg);
62
+ color: var(--tab-active-fg);
63
+ }
64
+
65
+ /* The active tab reads clearly above the inactive chips: a stronger fill.
66
+ Themeable (vibecli tints these purple). */
67
+ .wt-tab-active {
68
+ background: var(--tab-active-bg);
69
+ color: var(--tab-active-fg);
70
+ }
71
+
72
+ /* The label flexes to fill the fixed-width tab and truncates with an ellipsis
73
+ (min-width: 0 lets a flex item shrink below its content width). */
74
+ .wt-tab-label {
75
+ flex: 1 1 auto;
76
+ min-width: 0;
77
+ overflow: hidden;
78
+ text-overflow: ellipsis;
79
+ }
80
+
81
+ /* A little breathing room after the activity dot, before the label. */
82
+ .wt-tab-dot {
83
+ margin-right: 2px;
84
+ }
85
+
86
+ /* The tab close (x): a centered, muted square icon button with a box on hover.
87
+ Its glyph is an inline SVG (styled below), so flex-centering the button
88
+ centers it exactly. */
89
+ .wt-tab-close {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ flex: none;
94
+ width: 1.375rem;
95
+ height: 1.375rem;
96
+ padding: 0;
97
+ background: transparent;
98
+ border: none;
99
+ border-radius: var(--radius-inner);
100
+ color: color-mix(in oklch, var(--text) 60%, transparent);
101
+ cursor: pointer;
102
+ }
103
+
104
+ .wt-tab-close:hover {
105
+ background: var(--text-active);
106
+ color: var(--text);
107
+ }
108
+
109
+ /* On an inactive tab the close appears only on hover, kept in the layout via
110
+ visibility so the tab width never jumps; on the active tab it is always shown. */
111
+ .wt-tab:not(.wt-tab-active) .wt-tab-close {
112
+ visibility: hidden;
113
+ }
114
+
115
+ .wt-tab:not(.wt-tab-active):hover .wt-tab-close {
116
+ visibility: visible;
117
+ }
118
+
119
+ /* The add-tab (+) button: its hover box matches an active tab's fill and
120
+ stretches to the strip height; the 2px strip gap (not a custom margin) spaces
121
+ it from the last tab. Its glyph is an inline SVG. */
122
+ .wt-tab-new {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ flex: none;
127
+ /* Square box that matches the tab height (a stretched flex item ignores
128
+ aspect-ratio against its SVG's intrinsic width, so size it explicitly). The
129
+ tab height is the close-button (1.375rem) + vertical padding (2×sp-2) +
130
+ border. */
131
+ width: calc(1.375rem + 2 * var(--sp-2) + 2px);
132
+ height: calc(1.375rem + 2 * var(--sp-2) + 2px);
133
+ padding: 0;
134
+ background: transparent;
135
+ border: none;
136
+ border-radius: var(--radius-inner);
137
+ color: color-mix(in oklch, var(--text) 60%, transparent);
138
+ cursor: pointer;
139
+ }
140
+
141
+ .wt-tab-new:hover {
142
+ background: var(--tab-active-bg);
143
+ color: var(--tab-active-fg);
144
+ }
145
+
146
+ /* The +/x icons are inline SVG so they center exactly in their flex-centered
147
+ button and stay symmetric regardless of the UI font (fixes the off-center
148
+ glyph and the lopsided "+"). The + is a touch larger and bolder than the x. */
149
+ .wt-tab-close svg,
150
+ .wt-tab-new svg {
151
+ stroke: currentcolor;
152
+ fill: none;
153
+ stroke-width: 2.5;
154
+ stroke-linecap: round;
155
+ stroke-linejoin: round;
156
+ }
157
+
158
+ .wt-tab-close svg {
159
+ width: 13px;
160
+ height: 13px;
161
+ }
162
+
163
+ .wt-tab-new svg {
164
+ width: 16px;
165
+ height: 16px;
166
+ }
167
+
168
+ /* The tab being dragged (reorder) dims while it moves. */
169
+ .wt-tab-dragging {
170
+ opacity: 0.5;
171
+ }
172
+
173
+ /* Tab context menu (tabs feature, desktop right-click): Close / Close others /
174
+ Close to the right / Close to the left / Close all. Replaces the old bar
175
+ "Close all" button. Mirrors .wt-ctx-menu (a fixed surface popover, clamped
176
+ into the viewport by the feature); disabled items are dimmed and inert. */
177
+ .wt-tab-menu {
178
+ position: fixed;
179
+ display: none;
180
+ flex-direction: column;
181
+ background: var(--surface);
182
+ backdrop-filter: var(--blur);
183
+ border: 1px solid var(--border);
184
+ border-radius: var(--radius);
185
+ padding: var(--sp-1);
186
+ z-index: 30;
187
+ }
188
+
189
+ .wt-tab-menu.visible {
190
+ display: flex;
191
+ }
192
+
193
+ .wt-tab-menu button {
194
+ background: transparent;
195
+ color: var(--text);
196
+ border: none;
197
+ border-radius: var(--radius-inner);
198
+ font-family: var(--font-ui);
199
+ font-size: 0.8125rem;
200
+ font-weight: 500;
201
+ padding: var(--sp-1) var(--sp-3);
202
+ cursor: pointer;
203
+ text-align: left;
204
+ white-space: nowrap;
205
+ }
206
+
207
+ .wt-tab-menu button:not(:disabled):hover {
208
+ background: var(--text-active);
209
+ }
210
+
211
+ .wt-tab-menu button:disabled {
212
+ color: color-mix(in oklch, var(--text) 35%, transparent);
213
+ cursor: default;
214
+ }
215
+
216
+ /* Pull the terminal surface up off the fixed BOTTOM tab strip so it does not
217
+ overlap the last rows (a mirror of the old top offset). The tabs feature
218
+ marks the surface (.wt-with-tabbar) and keeps the measured strip height in
219
+ --wt-tabbar-h; the surface is position:fixed inset:0, so a bottom offset
220
+ clears it. Desktop only: on a coarse pointer the strip is hidden and the
221
+ mobile switcher's inset is applied through viewport.ts (--wt-reserve-bottom)
222
+ instead. */
223
+ @media (pointer: fine) {
224
+ .term.wt-with-tabbar {
225
+ bottom: var(--wt-tabbar-h, 44px);
226
+ }
227
+ }
228
+
229
+ /* ===== Activity dots (activityMonitor via tabs) =====
230
+ Four-state, distinguishable without color (shape + motion, not hue alone),
231
+ per section 12 / WCAG 1.4.1. The one .wt-status-dot vocabulary is shared by
232
+ the desktop strip, the mobile switcher, and the overview sheet rows.
233
+
234
+ Opt-in: the dot is hidden unless its container carries .wt-status, which the
235
+ tabs feature adds only when a status source (activityMonitor) is wired. A
236
+ generic bash/sh terminal has no activity concept, so its tabs read as clean
237
+ labels; the agent consumer (vibecli) supplies the monitor and gets the dots.
238
+ The element stays in the DOM either way (JS sets data-status regardless), so
239
+ revealing it is a pure CSS switch. */
240
+ .wt-status-dot {
241
+ display: none;
242
+ width: 9px;
243
+ height: 9px;
244
+ border-radius: 50%;
245
+ flex: none;
246
+ /* Default / idle (a new session with no turn yet): a hollow grey dot, so
247
+ "nothing has happened" reads as an outline rather than a filled state.
248
+ The fill (solid vs hollow) is the non-hue cue that separates the idle state
249
+ from the filled done/working/input states (WCAG 1.4.1). */
250
+ background: transparent;
251
+ border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
252
+ }
253
+
254
+ /* A status source is wired (the tabs feature marked the chrome .wt-status): show
255
+ the dots. Without this class the whole activity vocabulary stays invisible. */
256
+ .wt-status .wt-status-dot {
257
+ display: block;
258
+ }
259
+
260
+ .wt-status-dot[data-status="idle"] {
261
+ background: transparent;
262
+ border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
263
+ }
264
+
265
+ /* Agent working: solid pulsing white. (Was purple to match Kiro IDE's thinking
266
+ colour, but that collides with the purple active-tab fill; white reads clearly
267
+ on every tab state.) Motion is the primary non-hue cue. Server-side this is
268
+ driven by the OSC 9;4 progress signal, so it tracks agent activity, not the
269
+ user typing at the prompt. */
270
+ .wt-status-dot[data-status="working"] {
271
+ background: #fff;
272
+ border-color: #fff;
273
+ animation: wt-pulse 1.2s ease-in-out infinite;
274
+ }
275
+
276
+ /* Agent done / ready: solid green, matching Kiro IDE's "Ready" (#22c55e), no
277
+ motion. The filled dot distinguishes it from the hollow idle without hue. */
278
+ .wt-status-dot[data-status="done"] {
279
+ background: #22c55e;
280
+ border-color: #22c55e;
281
+ }
282
+
283
+ /* User action required (permission / approval): solid amber matching Kiro IDE's
284
+ "waiting-approval" (#fbbf24), plus a ring so it reads as "needs you" via shape
285
+ as well as hue. It persists until you act on or switch into the tab. */
286
+ .wt-status-dot[data-status="input"] {
287
+ background: #fbbf24;
288
+ border-color: #fbbf24;
289
+ box-shadow: 0 0 0 2px oklch(85% 0.16 85deg / 40%);
290
+ }
291
+
292
+ /* Exited: a dim hollow dot, static; distinct from idle by its lower opacity. */
293
+ .wt-status-dot[data-status="exited"] {
294
+ background: transparent;
295
+ border: 1.5px solid color-mix(in oklch, var(--text) 30%, transparent);
296
+ opacity: 0.5;
297
+ }
298
+
299
+ @keyframes wt-pulse {
300
+ 50% {
301
+ opacity: 0.4;
302
+ }
303
+ }
304
+
305
+ /* Reduced-motion (or the animations feature off): drop the pulse, keep color. */
306
+ @media (prefers-reduced-motion: reduce) {
307
+ .wt-status-dot[data-status="working"] {
308
+ animation: none;
309
+ }
310
+ }
@@ -0,0 +1,335 @@
1
+ /* ===== Mobile tab chrome (tabs feature, section 12) =====
2
+ The bottom bar (switcher) is the mobile tab surface, the externally-driven key
3
+ grid docks above it, and the overview sheet lists every tab. The desktop strip
4
+ is hidden on a coarse pointer and these appear instead; all motion (bar number
5
+ fade, sheet slide) lives in 07-animations.css. */
6
+
7
+ /* The switcher is hidden on a fine pointer, where the top strip is used instead;
8
+ the top strip is correspondingly hidden on a coarse pointer so tabs live in
9
+ the thumb zone. */
10
+ .wt-switcher {
11
+ display: none;
12
+ }
13
+
14
+ @media (pointer: coarse) {
15
+ .wt-tab-bar,
16
+ .wt-tab-new {
17
+ display: none;
18
+ }
19
+
20
+ .wt-switcher {
21
+ position: fixed;
22
+ left: 0;
23
+ right: 0;
24
+ /* Lift above the soft keyboard rather than hiding, so the switch surface
25
+ stays reachable in the terminal's primary working state (section 12). */
26
+ bottom: calc(var(--safe-bottom, 0px) + var(--kb-inset, 0px));
27
+ display: flex;
28
+ flex-direction: column; /* bar on top; the tab list expands below it as the bar slides up */
29
+ background: var(--surface);
30
+ backdrop-filter: var(--blur);
31
+ border-top: 1px solid var(--border);
32
+ z-index: 25;
33
+ }
34
+ }
35
+
36
+ /* The always-visible bar row: active tab + keyboard + "+". The padding gives the
37
+ active-tab chip breathing room above/below within the bar, matching the
38
+ desktop strip (both use --sp-2). */
39
+ .wt-switcher-bar {
40
+ display: flex;
41
+ align-items: stretch;
42
+ gap: var(--sp-1);
43
+ padding: var(--sp-2);
44
+ }
45
+
46
+ /* The other tabs, revealed below the bar row on expand (the bar slides up).
47
+ Collapsed to zero height; expanded caps the height and scrolls. The max-height
48
+ transition lives in 07-animations.css. */
49
+ .wt-switcher-list {
50
+ list-style: none;
51
+ margin: 0;
52
+ /* Match the bar's horizontal padding (--sp-2) so a listed row lines up with
53
+ the bar: its dot/label left-aligns with the active tab's, and its right edge
54
+ aligns with the "+" button's right edge (both inset --sp-2 from the switcher
55
+ edges). No separator border between the bar and the list — the active tab's
56
+ fill already sets it apart. */
57
+ padding: 0 var(--sp-2);
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 2px;
61
+ max-height: 0;
62
+ overflow: hidden;
63
+ /* max-height caps the whole box (incl. the expanded padding) so an interactive
64
+ pull from collapsed starts at a true zero height rather than a padding-tall
65
+ sliver. */
66
+ box-sizing: border-box;
67
+ }
68
+
69
+ .wt-switcher-expanded .wt-switcher-list {
70
+ max-height: 50vh;
71
+ overflow-y: auto;
72
+ padding-top: var(--sp-1);
73
+ padding-bottom: var(--sp-1);
74
+ }
75
+
76
+ /* The active-tab row: a select/swipe surface plus a close (x) overlaid at the
77
+ right, mirroring the listed rows (two buttons can't nest, so the close is a
78
+ sibling over the surface's reserved right padding). The wrap is the flex item
79
+ that grows in the bar (and fills the space when the keyboard button collapses
80
+ on expand). */
81
+ .wt-switcher-current-wrap {
82
+ position: relative;
83
+ flex: 1 1 auto;
84
+ display: flex;
85
+ min-width: 0;
86
+ /* Clip the swiping content (dot + label + close all translate on a swipe) at
87
+ the tab's edges, so the close slides off/in cleanly rather than spilling
88
+ over the "+" button. */
89
+ overflow: hidden;
90
+ }
91
+
92
+ .wt-switcher-current {
93
+ position: relative;
94
+ flex: 1 1 auto;
95
+ display: flex;
96
+ align-items: center;
97
+ gap: var(--sp-2);
98
+ min-height: var(--touch-target);
99
+ /* Right padding reserves room for the close (x) overlaid inside the row. */
100
+ padding: 0 var(--touch-target) 0 var(--sp-2);
101
+ /* The bar row always represents the active tab, so it wears the active-tab
102
+ fill/foreground the desktop strip uses (themeable; purple in vibecli). */
103
+ background: var(--tab-active-bg);
104
+ color: var(--tab-active-fg);
105
+ border: none;
106
+ border-radius: var(--radius-inner);
107
+ font-family: var(--font-ui);
108
+ font-size: 0.875rem;
109
+ cursor: pointer;
110
+ overflow: hidden;
111
+ /* Own both horizontal (switch) and vertical (swipe-up = overview) gestures.
112
+ The bar is fixed with nothing to scroll, so claiming the gesture is safe and
113
+ makes swipe detection reliable under passive listeners. */
114
+ touch-action: none;
115
+ }
116
+
117
+ /* The active row's close (x): overlaid at the right over the row's reserved
118
+ padding, mirroring the listed rows' close. Hover fill only on real hover
119
+ pointers so it never sticks white after a touch (a removed tab shifts the
120
+ list under the finger otherwise). */
121
+ .wt-switcher-current-close {
122
+ position: absolute;
123
+ /* Centre vertically via top/bottom (not a translateY transform), so transform
124
+ is free for the swipe translate — the close moves with the dot + label. And
125
+ no right offset, so it sits flush at the tab's right edge (no gap). */
126
+ top: 0;
127
+ bottom: 0;
128
+ right: 0;
129
+ color: color-mix(in oklch, var(--text) 55%, transparent);
130
+ }
131
+
132
+ @media (hover: hover) {
133
+ .wt-switcher-current-close:hover {
134
+ color: var(--text);
135
+ }
136
+ }
137
+
138
+ /* Press feedback: brighten the active fill slightly (a lighter press over an
139
+ already-filled row). */
140
+ .wt-switcher-current:active {
141
+ background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
142
+ }
143
+
144
+ /* The row's content (dot + label + position). It fills the row and lays its
145
+ parts out; a horizontal swipe translates it (clipped by the row's
146
+ overflow: hidden) so the active tab area physically slides with the finger,
147
+ and the incoming tab's label slides in from the swipe direction on commit. */
148
+ .wt-switcher-current-inner {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: var(--sp-2);
152
+ flex: 1 1 auto;
153
+ min-width: 0;
154
+ }
155
+
156
+ .wt-switcher-label {
157
+ flex: 1 1 auto;
158
+ overflow: hidden;
159
+ text-overflow: ellipsis;
160
+ white-space: nowrap;
161
+ text-align: left;
162
+ }
163
+
164
+ /* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
165
+ grid is open so its toggled state reads at a glance. An explicit width (not
166
+ just the wt-btn min-width) makes it animatable: when the tab list expands the
167
+ button collapses to zero width and fades out (the key grid is closed too, in
168
+ JS), and the active row flex-grows to fill the freed space. */
169
+ .wt-switcher-kb {
170
+ width: var(--touch-target);
171
+ /* Override .wt-btn's min-width (the 44px tap floor). min-width is NOT
172
+ transitioned, so on collapse it would revert to 44px instantly and clamp the
173
+ animating width back to full — the button snapped to size on close while
174
+ animating smoothly on open. The 44px open size still holds via `width`. */
175
+ min-width: 0;
176
+ /* Drop the user-agent button padding: with border-box it would floor the
177
+ collapsed width at the padding (≈12px) instead of a clean 0. The icon
178
+ centres via wt-btn. */
179
+ padding: 0;
180
+ overflow: hidden;
181
+ }
182
+
183
+ .wt-switcher-kb[hidden] {
184
+ display: none;
185
+ }
186
+
187
+ .wt-switcher-kb.wt-active {
188
+ background: var(--tab-hover-bg);
189
+ color: var(--tab-active-fg);
190
+ }
191
+
192
+ /* Sticky-Ctrl armed: invert the keyboard button (the same solid fill the armed
193
+ Ctrl key uses) while a Ctrl press is pending, so the modifier stays visible
194
+ even with the key grid — and its own Ctrl button — closed. After .wt-active so
195
+ the armed fill wins when the grid is also open. */
196
+ .wt-switcher-kb.wt-armed {
197
+ background: var(--text);
198
+ color: var(--bg);
199
+ }
200
+
201
+ .wt-switcher-expanded .wt-switcher-kb {
202
+ width: 0;
203
+ /* Absorb the flex gap the collapsed button would otherwise leave behind. */
204
+ margin-inline-start: calc(-1 * var(--sp-1));
205
+ opacity: 0;
206
+ pointer-events: none;
207
+ }
208
+
209
+ /* Mobile "+": the primary action, carried in the accent colour so it reads as
210
+ permanent and prominent. Hover fill is gated to real hover pointers: on touch,
211
+ :hover sticks after a tap (no pointer-leave), so the purple would linger — the
212
+ press feedback there comes from :active, which clears on release. */
213
+ .wt-switcher-new {
214
+ flex: none;
215
+ color: var(--accent);
216
+ }
217
+
218
+ @media (hover: hover) {
219
+ .wt-switcher-new:hover {
220
+ background: var(--tab-hover-bg);
221
+ }
222
+ }
223
+
224
+ .wt-switcher-new:active {
225
+ background: var(--tab-hover-bg);
226
+ }
227
+
228
+ /* Aggregate needs-input attention now rides the active surface (the separate
229
+ overview button is gone): a background tab is blocked on you, and a tap /
230
+ swipe-up here opens the overview to resolve it. Shape + color (an inset ring
231
+ plus a corner glyph), never hue alone (WCAG 1.4.1). */
232
+ .wt-switcher-current[data-attention="input"] {
233
+ box-shadow: inset 0 0 0 2px oklch(70% 0.18 25deg / 45%);
234
+ }
235
+
236
+ .wt-switcher-current[data-attention="input"]::after {
237
+ content: "";
238
+ position: absolute;
239
+ top: 4px;
240
+ right: 4px;
241
+ width: 8px;
242
+ height: 8px;
243
+ border-radius: 50%;
244
+ background: oklch(70% 0.18 25deg);
245
+ }
246
+
247
+ /* ===== Externally-driven key grid (tabbed presets, WS3) =====
248
+ In the tabbed presets the mobile toolbar hides its own toggle (the tab bar's
249
+ keyboard button drives it) and the grid docks above the bar on the right, to
250
+ the left of where scroll-to-bottom sits. It collapses to fully hidden, since
251
+ there is no in-grid toggle to keep visible. */
252
+ @media (pointer: coarse) {
253
+ .wt-toolbar-external .kb-toggle {
254
+ display: none;
255
+ }
256
+
257
+ .key-toolbar.wt-toolbar-external {
258
+ top: auto;
259
+ right: calc(var(--sp-3) + var(--touch-target) + var(--sp-2));
260
+ bottom: calc(
261
+ var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
262
+ );
263
+ grid-template-columns: repeat(4, var(--touch-target));
264
+ }
265
+
266
+ .key-toolbar.wt-toolbar-external.collapsed {
267
+ display: none;
268
+ }
269
+ }
270
+
271
+ /* ===== Expanded tab-list rows (tabs feature) =====
272
+ One row per OTHER tab in the expanded list: a stretched select target (dot +
273
+ label) with the close (x) laid inside at the right, mirroring the desktop
274
+ tab's merged chrome. */
275
+ .wt-switcher-row {
276
+ position: relative;
277
+ display: flex;
278
+ }
279
+
280
+ .wt-switcher-row-select {
281
+ flex: 1 1 auto;
282
+ display: flex;
283
+ align-items: center;
284
+ gap: var(--sp-2);
285
+ min-height: var(--touch-target);
286
+ /* Right padding reserves room for the close (x) overlaid inside the box. */
287
+ padding: 0 var(--touch-target) 0 var(--sp-2);
288
+ background: transparent;
289
+ color: var(--text);
290
+ border: none;
291
+ border-radius: var(--radius-inner);
292
+ font-family: var(--font-ui);
293
+ font-size: 0.875rem;
294
+ text-align: left;
295
+ cursor: pointer;
296
+ overflow: hidden;
297
+ }
298
+
299
+ @media (hover: hover) {
300
+ .wt-switcher-row-select:hover {
301
+ background: var(--tab-hover-bg);
302
+ }
303
+ }
304
+
305
+ /* Press / touch-hold feedback on a listed (inactive) tab: the same subtle fill
306
+ the "+" button uses (--tab-hover-bg), NOT the stronger active-tab fill, so an
307
+ inactive row never looks like the active one. */
308
+ .wt-switcher-row-select:active {
309
+ background: var(--tab-hover-bg);
310
+ }
311
+
312
+ .wt-switcher-row-label {
313
+ flex: 1 1 auto;
314
+ overflow: hidden;
315
+ text-overflow: ellipsis;
316
+ white-space: nowrap;
317
+ }
318
+
319
+ /* The close sits flush at the row's right edge (no gap), vertically centered. */
320
+ .wt-switcher-row-close {
321
+ position: absolute;
322
+ top: 50%;
323
+ right: 0;
324
+ transform: translateY(-50%);
325
+ color: color-mix(in oklch, var(--text) 55%, transparent);
326
+ }
327
+
328
+ /* Hover only on real hover pointers: on touch, :hover sticks after a tap, so
329
+ when closing a tab shifts the list under the finger the newly-under close
330
+ would latch white. Touch feedback comes from the surrounding row's :active. */
331
+ @media (hover: hover) {
332
+ .wt-switcher-row-close:hover {
333
+ color: var(--text);
334
+ }
335
+ }