anvilsign in

collin/browser-terminal-extension

1/* Every colour comes from lib/theme.js, applied to :root as custom properties
2 at startup. The fallbacks here are the dark theme, so the panel is never
3 unstyled during the first paint. */
4:root {
5 color-scheme: dark;
6 --bg: #14161a;
7 --bg-panel: #1c1f25;
8 --bg-input: #14161a;
9 --border: #2d3138;
10 --fg: #d7dae0;
11 --fg-muted: #8b93a1;
12 --accent: #3b6ea5;
13 --accent-hover: #4a82bd;
14 --accent-fg: #ffffff;
15 --ok: #6ec177;
16 --err: #e06c75;
17 --warn: #d5a458;
18 --warn-bg: #241f16;
19 --warn-border: #4a3a22;
20 --shadow: rgba(0, 0, 0, 0.4);
21}
22* { box-sizing: border-box; }
23/* Author `display` beats the UA rule for [hidden] no matter the specificity, so
24 every flex/inline-flex element in here would otherwise ignore .hidden = true.
25 Everything that toggles itself does it through the attribute; this is what
26 makes that work. */
27[hidden] { display: none !important; }
28html, body { height: 100%; margin: 0; }
29body {
30 font: 13px/1.5 system-ui, sans-serif;
31 background: var(--bg); color: var(--fg);
32 display: flex; flex-direction: column; overflow: hidden;
33}
34/* Vertical space is scarce in a sidebar, but not so scarce that controls
35 should become unclickable. 26px is about the smallest comfortable pointer
36 target; the header stays short by having only two of them. */
37/* Stretch, not centre: the active tab has to reach the bottom edge to sit on
38 the seam. Everything else in the row re-centres itself. */
39header {
40 display: flex; align-items: stretch; gap: 4px;
41 padding: 4px 6px 0; background: var(--bg-panel);
42 border-bottom: 1px solid var(--border); flex: none;
43}
44#status-text {
45 flex: 1; font-size: 11px; color: var(--fg-muted); min-width: 0;
46 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
47}
48.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
49
50/* The tabs are the only thing in this row that reaches the seam; everything
51 else is centred on the same 22px band, so the dot, the session name and the
52 icons sit on one line rather than each on its own accidental one. The 4px
53 bottom margin is the tabs' own gap above the seam, matched here. */
54header > #session-chip,
55header > #status-text,
56header > button.icon {
57 align-self: center;
58 margin-bottom: 4px;
59}
60header > #status-text,
61header > #session-chip { height: 22px; }
62header > #status-text { line-height: 20px; }
63
64/* The session chip. Without a session there is nothing to switch to, so it is
65 just the status dot sitting where it always did; with one it closes up into a
66 recessed pill. The grey is mixed from the row rather than taken from
67 --bg-input, which in the light theme is the same white as the terminal — the
68 chip would then read as a lifted tab, which is the whole thing being avoided.
69 The extra right margin is the gap that says the tabs are a different group. */
70#session-chip {
71 display: inline-flex; align-items: center; gap: 5px;
72 flex: 0 1 auto; min-width: 0;
73 margin-left: 2px;
74 border: 1px solid transparent; border-radius: 999px;
75}
76body.has-session #session-chip {
77 padding: 0 5px 0 7px; margin-right: 9px;
78 background: rgba(128, 128, 128, 0.13);
79 border-color: var(--border);
80}
81body.has-session #session-chip:hover { background: rgba(128, 128, 128, 0.2); }
82#session-chip:focus-within { border-color: var(--accent); }
83
84/* Shrinkable: in a narrow panel the tabs are what the row is for, and the
85 session name is already in the select's own tooltip. The select draws no box
86 of its own — the chip around it is the box. */
87#session-switch {
88 flex: 0 1 auto; min-width: 3rem; max-width: 9rem;
89 font: inherit; font-size: 11px; font-weight: 500; line-height: 20px;
90 padding: 0; border: 0; border-radius: 0;
91 background: transparent; color: var(--fg);
92 text-overflow: ellipsis;
93}
94#session-switch:focus-visible { outline: none; }
95
96/* --- window tabs -----------------------------------------------------------
97 A tab is a window of the attached session — what `prefix 2` selects. Clicking
98 one is a select-window: nothing in this strip spawns or destroys a shell
99 except "+", which opens a window. */
100/* Sized to its tabs, not to the row: "+" sits against the last tab the way a
101 browser's does, and the strip gives up width to scrolling before the row
102 gives up its buttons. */
103/* The strip has to reach over the header's bottom border, because the active
104 tab's whole trick is covering the 1px of it that it sits on. `overflow-x:
105 auto` computes overflow-y to `auto` as well, so a scroll container clips its
106 children at the padding box no matter which axis you asked for: without the
107 margin/padding pair below, the tab's overhang is cut off at the seam and the
108 line runs straight through the one tab that is meant to break it.
109 The margin grows the box downwards onto the border, the padding puts the
110 tabs' own bottom edge back where it was, and the 1px between the two is room
111 the overhang is allowed to paint in. */
112#tabs {
113 display: flex; align-items: stretch; gap: 2px;
114 flex: 0 1 auto; min-width: 0;
115 margin-bottom: -1px; padding-bottom: 1px;
116 overflow-x: auto; scrollbar-width: none;
117}
118#tabs::-webkit-scrollbar { display: none; }
119/* Nothing follows the tabs, so this is just what stops "+" from being stretched
120 away from the last one. */
121#tab-new { margin-right: auto; }
122
123/* The panel's controls are one group and the session is another; the row's 4px
124 gap is not enough to say so. */
125header > #settings-toggle { margin-right: 5px; }
126
127/* The tab and its close ✕ are siblings — a button cannot contain a button —
128 so the slot is what the strip lays out, and the ✕ is positioned over the
129 tab's right edge.
130
131 Past the min width a name is unreadable, so the strip scrolls instead of
132 shrinking further: the same trade browsers make when the bar fills up. */
133.tab-slot {
134 position: relative; display: flex; align-items: stretch;
135 flex: 0 1 auto; min-width: 5.5rem; max-width: 11rem;
136}
137/* The window you are on gives up its characters last. */
138.tab-slot.active { min-width: 7rem; }
139
140button.tab {
141 flex: 1 1 auto; min-width: 0;
142 display: inline-flex; align-items: center; gap: 5px;
143 padding: 4px 8px 5px; margin-bottom: 4px;
144 border: 0; border-radius: 6px 6px 0 0;
145 background: transparent; color: var(--fg-muted);
146 font: inherit; font-size: 11px; line-height: 1.4;
147 white-space: nowrap; cursor: pointer; text-align: left;
148}
149button.tab:hover { background: rgba(128, 128, 128, 0.16); color: var(--fg); }
150button.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
151button.tab .name { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
152button.tab .index {
153 font-size: 9.5px; opacity: .6; font-variant-numeric: tabular-nums; flex: none;
154}
155/* tmux's own "something happened here while you were away". */
156button.tab.activity { color: var(--fg); }
157button.tab.activity .name { font-weight: 600; }
158
159/* The favicon slot: what Claude Code is doing in that window, and nothing at
160 all for a window that is just a shell — an always-lit glyph would be a status
161 light reporting on nothing.
162
163 The glyph is Claude Code's own asterisk spinner, so a working window in the
164 strip and the transcript in the pane are saying the same thing the same way.
165 The box is sized and centred once, for the widest frame, so the name beside
166 it does not step sideways as the animation cycles. */
167button.tab .glyph {
168 flex: none;
169 width: 10px; height: 11px;
170 display: inline-flex; align-items: center; justify-content: center;
171 font-size: 10px; line-height: 1;
172 color: var(--fg-muted); opacity: .5;
173}
174button.tab .glyph.none { display: none; }
175button.tab .glyph.working { color: var(--warn); opacity: 1; }
176button.tab .glyph.waiting { color: var(--accent-hover); opacity: 1; animation: pulse 1.4s ease-in-out infinite; }
177
178/* The lift: the terminal's background, shoulders drawn with an inset border,
179 and a negative bottom margin that punches through the header's seam so the
180 tab and the terminal read as one surface. */
181button.tab[aria-selected="true"] {
182 background: var(--bg); color: var(--fg);
183 margin-bottom: -1px; padding-bottom: 10px;
184 box-shadow: inset 1px 1px 0 0 var(--border), inset -1px 0 0 0 var(--border);
185}
186button.tab[aria-selected="true"]:hover { background: var(--bg); }
187
188/* Room for the ✕ is reserved rather than made on hover, so a name does not
189 re-ellipsis under the pointer. */
190.tab-slot.closable button.tab { padding-right: 20px; }
191
192/* Pinned: down to the dot and the index, sized to its content and sitting at
193 the head of the strip. The index is what stays visible rather than the name,
194 because the index is the thing you can type — `prefix 3` still selects it,
195 pinned or not. */
196.tab-slot.pinned { flex: 0 0 auto; min-width: 0; max-width: none; }
197.tab-slot.pinned button.tab { padding-left: 7px; padding-right: 7px; gap: 4px; }
198.tab-slot.pinned button.tab .index { opacity: 1; font-size: 11px; }
199/* A hairline against the unpinned run, the way a browser separates them. */
200.tab-slot.pinned + .tab-slot:not(.pinned) { margin-left: 5px; }
201.tab-slot.pinned + .tab-slot:not(.pinned)::before {
202 content: ""; position: absolute; left: -3px; top: 6px; bottom: 6px;
203 border-left: 1px solid var(--border);
204}
205
206/* Right-click menu. A plain positioned element: an extension page would
207 otherwise get the browser's own menu, which has nothing to say about tmux. */
208.tab-menu {
209 position: fixed; z-index: 20; min-width: 8rem;
210 display: flex; flex-direction: column; padding: 3px;
211 background: var(--bg-panel); color: var(--fg);
212 border: 1px solid var(--border); border-radius: 6px;
213 box-shadow: 0 6px 20px var(--shadow);
214}
215.tab-menu button {
216 background: transparent; color: var(--fg);
217 border: 0; border-radius: 4px; padding: 5px 8px;
218 font: inherit; font-size: 11px; text-align: left; cursor: pointer;
219}
220.tab-menu button:hover { background: var(--border); }
221.tab-menu button.danger { color: var(--err); }
222.tab-menu button.danger:hover { background: var(--err); color: var(--accent-fg); }
223.tab-menu button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
224
225button.tab-close {
226 position: absolute; right: 4px; top: 5px;
227 width: 15px; height: 15px; padding: 0;
228 display: inline-flex; align-items: center; justify-content: center;
229 border: 0; border-radius: 4px;
230 background: transparent; color: var(--fg-muted); cursor: pointer;
231 opacity: 0; pointer-events: none; transition: opacity 90ms linear;
232}
233button.tab-close svg { width: 9px; height: 9px; display: block; }
234/* On the tab you are on and the one you are pointing at — a full strip of ✕ is
235 noise, and every one of them is destructive. */
236.tab-slot.active button.tab-close,
237.tab-slot:hover button.tab-close,
238.tab-slot:focus-within button.tab-close { opacity: 1; pointer-events: auto; }
239/* Red on hover rather than the usual grey: this is the one control in the
240 panel that destroys something, and it takes effect on the first click. */
241button.tab-close:hover { background: var(--err); color: var(--accent-fg); }
242button.tab-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; }
243
244
245button.icon {
246 flex: none;
247 width: 26px; height: 26px; padding: 0;
248 display: inline-flex; align-items: center; justify-content: center;
249 background: transparent; border: 0; border-radius: 5px;
250 color: var(--fg-muted); cursor: pointer;
251}
252button.icon svg { width: 15px; height: 15px; display: block; }
253button.icon:hover { background: var(--border); color: var(--fg); }
254button.icon:active { transform: translateY(0.5px); }
255button.icon:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
256#pick.active { background: var(--accent); color: var(--accent-fg); }
257
258/* The session name lives in the switcher once there is one, so the status text
259 stands down rather than saying it twice. The dot stays: it is the only thing
260 reporting on the connection itself. */
261body.has-session > header > #status-text { display: none; }
262
263/* Compact: lose the status text, keep the targets clickable. */
264body.compact header { padding: 2px 4px 0; }
265body.compact #status-text { display: none; }
266body.compact button.icon { width: 24px; height: 24px; }
267body.compact > header > #session-chip { height: 20px; }
268body.compact #session-switch { line-height: 18px; }
269body.compact .tab-slot { max-width: 8rem; }
270body.compact button.tab { padding: 3px 6px 4px; }
271body.compact .tab-slot.closable button.tab { padding-right: 19px; }
272body.compact button.tab[aria-selected="true"] { padding-bottom: 9px; }
273body.compact button.tab .index { display: none; }
274body.compact button.tab-close { top: 4px; }
275body.compact #term { padding: 2px; }
276
277/* Hidden: no header at all. A 4px strip along the top brings it back on
278 hover, so the settings panel is never unreachable. */
279body.chromeless header {
280 position: absolute; top: 0; left: 0; right: 0; z-index: 5;
281 opacity: 0; pointer-events: none; transition: opacity 90ms linear;
282}
283body.chromeless #reveal-strip {
284 display: block; position: absolute; top: 0; left: 0; right: 0;
285 height: 4px; z-index: 6;
286}
287body.chromeless #reveal-strip:hover ~ header,
288body.chromeless header:hover { opacity: 1; pointer-events: auto; }
289body.chromeless #term { padding: 2px; }
290#reveal-strip { display: none; }
291
292.dot.on { background: var(--ok); }
293.dot.off { background: var(--err); }
294.dot.pending { background: var(--warn); }
295
296@keyframes pulse { 50% { opacity: .25; } }
297/* The spinner is driven from script, which parks it on a single frame when
298 motion is reduced; this is only the waiting glyph's own breathing. */
299@media (prefers-reduced-motion: reduce) {
300 button.tab .glyph.waiting { animation: none; }
301}
302#term { flex: 1; min-height: 0; padding: 4px; overflow: hidden; }
303#term .xterm { height: 100%; }
304aside {
305 flex: none; max-height: 55%; overflow-y: auto;
306 padding: 10px; background: var(--bg-panel); border-top: 1px solid var(--border);
307}
308h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
309 color: var(--fg-muted); margin: 12px 0 4px; font-weight: 600; }
310h2:first-child { margin-top: 0; }
311.hint { color: var(--fg-muted); margin: 0 0 6px; font-size: 11px; }
312.row { display: flex; gap: 6px; align-items: center; }
313input[type=text], input[type=password], input:not([type]), #url {
314 width: 100%; background: var(--bg-input); color: var(--fg);
315 border: 1px solid var(--border); border-radius: 4px; padding: 5px 7px;
316 font-family: ui-monospace, monospace; font-size: 11px;
317}
318button {
319 background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 4px;
320 padding: 5px 10px; font-size: 11px; cursor: pointer;
321}
322button:hover { background: var(--accent-hover); }
323button.ghost { background: transparent; border: 1px solid var(--border);
324 color: var(--fg-muted); padding: 3px 7px; }
325button.ghost:hover { background: var(--border); }
326pre {
327 background: var(--bg-input); border: 1px solid var(--border); border-radius: 4px;
328 padding: 6px; margin: 0 0 6px; overflow-x: auto;
329 font-family: ui-monospace, monospace; font-size: 10px;
330 white-space: pre-wrap; word-break: break-all;
331}
332#log { max-height: 120px; overflow-y: auto; color: var(--fg-muted); }
333code { font-family: ui-monospace, monospace; background: var(--bg-input);
334 padding: 1px 3px; border-radius: 3px; font-size: 10px; }
335label.row { color: var(--fg-muted); font-size: 11px; margin-top: 5px; }
336label.row input { width: auto; }
337
338#trust-hint { margin-top: 8px; padding: 8px; border: 1px solid var(--warn-border);
339 background: var(--warn-bg); border-radius: 4px; }
340.hint.warn { color: var(--warn); }
341
342/* Picked-element panel. A results card, not a toolbar: one primary action,
343 the rest reduced to icons so the value itself is what draws the eye. */
344#picked {
345 flex: none;
346 background: var(--bg-panel);
347 border-bottom: 1px solid var(--border);
348 border-top: 2px solid var(--accent);
349 padding: 7px 8px 9px;
350}
351
352.picked-top { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
353.chip {
354 font-family: ui-monospace, monospace; font-size: 11px;
355 color: var(--accent); background: var(--bg-input);
356 border: 1px solid var(--border); border-radius: 4px;
357 padding: 1px 6px; flex: none;
358}
359#picked-meta {
360 flex: 1; min-width: 0; font-size: 11px; color: var(--fg-muted);
361 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
362}
363.picked-top button.icon { width: 22px; height: 22px; }
364.picked-top button.icon svg { width: 13px; height: 13px; }
365
366/* Segmented control instead of a native <select>: the options are few, short,
367 and worth seeing all at once. */
368.segmented {
369 display: flex; gap: 2px; margin-bottom: 6px;
370 background: var(--bg-input);
371 border: 1px solid var(--border); border-radius: 6px; padding: 2px;
372}
373.segmented button {
374 flex: 1 1 0; min-width: 0;
375 background: transparent; color: var(--fg-muted);
376 border: 0; border-radius: 4px; padding: 3px 4px;
377 font-size: 10px; font-weight: 500; letter-spacing: .02em;
378 cursor: pointer; white-space: nowrap;
379 overflow: hidden; text-overflow: ellipsis;
380}
381.segmented button:hover { color: var(--fg); background: var(--border); }
382.segmented button[aria-selected="true"] {
383 background: var(--accent); color: var(--accent-fg);
384}
385.segmented button[aria-selected="true"]:hover { background: var(--accent-hover); }
386
387#picked-value {
388 background: var(--bg-input);
389 border: 1px solid var(--border); border-radius: 6px;
390 padding: 7px 8px;
391 font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.45;
392 color: var(--fg);
393 word-break: break-all; white-space: pre-wrap;
394 max-height: 84px; overflow-y: auto;
395 user-select: all;
396}
397#picked-value.empty { color: var(--fg-muted); font-style: italic; user-select: none; }
398
399#picked-insert { width: 100%; margin-top: 7px; padding: 6px 10px; font-size: 11px; }
400#picked-warn { margin: 6px 0 0; font-size: 11px; }
401#picked-grant { width: 100%; margin-top: 6px; }
402
403select { font-family: inherit; }
404
405#density-hint { margin-top: 6px; }
406
407#font-row { margin-top: 7px; }
408#font-row .hint { margin: 0; margin-right: auto; }
409#font-row button.icon { width: 24px; height: 24px; border: 1px solid var(--border); }
410#font-size { font-variant-numeric: tabular-nums; font-size: 11px; min-width: 2ch;
411 text-align: center; color: var(--fg); }
412#font-reset { padding: 4px 8px; font-size: 11px; }
413#theme-select, #density-select { width: auto; background: var(--bg-input); color: var(--fg);
414 border: 1px solid var(--border); border-radius: 4px; padding: 5px 7px; font-size: 11px; }