| 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 | --bg-strip: #191b21; |
| 22 | /* The window row's surface, between the session row's and the terminal's. |
| 23 | Named because two things have to agree on it exactly: the row itself, and |
| 24 | the selected session tab that merges into it. A tab a shade off the row it |
| 25 | sits on shows the join as a visible edge. */ |
| 26 | --bg-row: color-mix(in srgb, var(--bg-panel) 55%, var(--bg-strip)); |
| 27 | /* The tab silhouette's two radii, in Chromium's proportions: it draws a 28px |
| 28 | tab with a 12px bottom endcap and a 10px top corner (TabStyle:: |
| 29 | GetBottomCornerRadius / GetTopCornerRadius), so a ~25px tab gets 10 and 8. |
| 30 | --tab-foot is also how far the tab's body is inset from its own box: the |
| 31 | feet are part of the tab, not something it hangs into the row. */ |
| 32 | --tab-foot: 10px; |
| 33 | --tab-top: 8px; |
| 34 | /* 2 * --tab-foot less the slot the separator lives in (Chromium: |
| 35 | `2 * GetBottomCornerRadius() - total_separator_width`). */ |
| 36 | --tab-slot-overlap: 14px; |
| 37 | /* What is left between two tab bodies once they overlap: the separator's |
| 38 | lane. */ |
| 39 | --tab-gap: calc(2 * var(--tab-foot) - var(--tab-slot-overlap)); |
| 40 | } |
| 41 | * { box-sizing: border-box; } |
| 42 | /* Author `display` beats the UA rule for [hidden] no matter the specificity, so |
| 43 | every flex/inline-flex element in here would otherwise ignore .hidden = true. |
| 44 | Everything that toggles itself does it through the attribute; this is what |
| 45 | makes that work. */ |
| 46 | [hidden] { display: none !important; } |
| 47 | html, body { height: 100%; margin: 0; } |
| 48 | body { |
| 49 | font: 13px/1.5 system-ui, sans-serif; |
| 50 | background: var(--bg); color: var(--fg); |
| 51 | display: flex; flex-direction: column; overflow: hidden; |
| 52 | } |
| 53 | /* Vertical space is scarce in a sidebar, but not so scarce that controls |
| 54 | should become unclickable. 26px is about the smallest comfortable pointer |
| 55 | target; the header stays short by having few of them, spread over its two |
| 56 | rows rather than crowding either. |
| 57 | |
| 58 | Two rows: sessions, then the windows of the selected one. #rows is the |
| 59 | column; each strip lays out the way the single row used to. The header itself |
| 60 | is a *row*, because one control — push to talk — sits beside all of them in a |
| 61 | corner of its own rather than inside any one of them. |
| 62 | |
| 63 | The header's own background is the terminal's, not the panel's: every strip |
| 64 | paints its own surface over it, and what is left uncovered is the corner — |
| 65 | which should read as a continuation of the row it sits beside rather than as |
| 66 | a slab of a fourth colour laid over the end of them. */ |
| 67 | header { |
| 68 | display: flex; align-items: stretch; |
| 69 | background: var(--bg); flex: none; |
| 70 | } |
| 71 | #rows { |
| 72 | display: flex; flex-direction: column; |
| 73 | flex: 1; min-width: 0; |
| 74 | } |
| 75 | /* A column reserved across every row, holding the one control that types into |
| 76 | the pane rather than steering the tabs: hold-to-talk, and nothing else — the |
| 77 | hold sends its own Return, so there is no second button to stack under it. |
| 78 | Unpainted, so it takes the header's background and reads as an extension of |
| 79 | the bottom row instead of a slab of its own — the rows are tmux and their |
| 80 | surfaces are the nesting; a fourth colour beside them was one surface too |
| 81 | many, and it was the one thing in the header with a hard vertical seam down |
| 82 | it. |
| 83 | |
| 84 | Its width is its own — a square is what the mic wants, and the rows have no |
| 85 | say in it — while its height is whatever the rows come to. Not the other way |
| 86 | round: sizing the button off the rows' height and the corner off the button |
| 87 | is a loop, and the browser breaks it by shrinking the strips. Which is why |
| 88 | the mic below is sized *from* that height rather than fixed: the header has |
| 89 | one row or two depending on the mode, and the corner is as tall as either. |
| 90 | |
| 91 | `gap` and the bottom padding are the mic's own air, not a gutter between |
| 92 | stacked controls: it is centred in the corner and the padding keeps its ring |
| 93 | off the terminal's edge while it is held. */ |
| 94 | #corner { |
| 95 | flex: none; width: 56px; |
| 96 | display: flex; flex-direction: column; |
| 97 | align-items: center; justify-content: center; gap: 3px; |
| 98 | padding: 3px 4px 4px; |
| 99 | /* Not a flat fill: beside the top row the header's surface is the strips', |
| 100 | and by the bottom it is the terminal's, so the corner falls from one to the |
| 101 | other instead of meeting both with the same edge. The stops are soft on |
| 102 | purpose — the exact row heights are not knowable here, and a gradient that |
| 103 | tried to line up with them would be wrong in the mode that has one row. */ |
| 104 | background: linear-gradient( |
| 105 | to bottom, |
| 106 | var(--bg-row), |
| 107 | color-mix(in srgb, var(--bg-row) 35%, var(--bg)) 55%, |
| 108 | var(--bg) 85% |
| 109 | ); |
| 110 | } |
| 111 | /* Stretch, not centre: the active window tab has to reach the bottom edge to |
| 112 | sit on the seam. Everything else in the row re-centres itself. */ |
| 113 | .strip { |
| 114 | display: flex; align-items: stretch; min-width: 0; |
| 115 | /* No gap: an icon button already carries its own padding around a 15px glyph, |
| 116 | so a gap on top of that reads as a hole between the controls and the tabs. |
| 117 | The buttons sit against the tab strip the way a browser's do. */ |
| 118 | gap: 0; |
| 119 | padding: 4px 6px 0 4px; |
| 120 | } |
| 121 | /* The controls that aren't buttons still need their own air. */ |
| 122 | .strip > #status-text { margin-left: 6px; } |
| 123 | .strip > #session-name { margin-left: 4px; } |
| 124 | /* Three surfaces, deepest at the top: the session row is recessed behind the |
| 125 | window row, which is recessed behind the terminal. That is what makes the |
| 126 | nesting readable without a single dividing line — the selected tab of each |
| 127 | row is painted in the surface of the row *below* it, so it merges into what |
| 128 | it contains, the way a browser's active tab merges into its toolbar. |
| 129 | How deep that recession reads is a theme's call, not a formula's: mixing it |
| 130 | from --bg-panel put dark's three surfaces within a few units of each other |
| 131 | while light's stayed obvious, so it is a token both themes name. */ |
| 132 | #session-strip { background: var(--bg-strip); } |
| 133 | #window-strip { background: var(--bg-row); } |
| 134 | #status-text { |
| 135 | flex: 1; font-size: 11px; color: var(--fg-muted); min-width: 0; |
| 136 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| 137 | } |
| 138 | |
| 139 | /* The tabs are the only thing in a strip that reaches its bottom edge; |
| 140 | everything else is centred on the same 22px band, so the status text and the |
| 141 | icons sit on one line rather than each on its own accidental one. The 4px |
| 142 | bottom margin is the tabs' own gap above that edge, matched. */ |
| 143 | .strip > #status-text, |
| 144 | .strip > #session-name, |
| 145 | .strip > button.icon { |
| 146 | align-self: center; |
| 147 | margin-bottom: 4px; |
| 148 | } |
| 149 | .strip > #status-text { height: 22px; line-height: 20px; } |
| 150 | |
| 151 | /* Naming a new session, inline in the row the tab will appear in. A prompt or |
| 152 | a dialog would block the socket's message handler for as long as it is up. */ |
| 153 | #session-name { |
| 154 | flex: 0 1 9rem; min-width: 4rem; height: 22px; |
| 155 | font: inherit; font-size: 11px; |
| 156 | padding: 0 6px; border: 1px solid var(--accent); border-radius: 6px; |
| 157 | background: var(--bg-input); color: var(--fg); |
| 158 | } |
| 159 | #session-name:focus-visible { outline: none; } |
| 160 | |
| 161 | /* --- session tabs ---------------------------------------------------------- |
| 162 | A tab is a whole tmux session. Clicking one is a switch-client: it moves the |
| 163 | client this panel already holds, so nothing is spawned and nothing is lost. |
| 164 | The selected one lifts onto the window row below it, which is the same move |
| 165 | the window row makes onto the terminal — one gesture, applied twice, so the |
| 166 | nesting is legible from the shapes alone. */ |
| 167 | #sessions { |
| 168 | display: flex; align-items: stretch; gap: 0; |
| 169 | flex: 0 1 auto; min-width: 0; |
| 170 | overflow-x: auto; scrollbar-width: none; |
| 171 | } |
| 172 | #sessions::-webkit-scrollbar { display: none; } |
| 173 | /* Sized to its tabs the way the window row's "+" is: the row gives up width to |
| 174 | scrolling before it gives up the button. */ |
| 175 | #session-new { margin-right: auto; } |
| 176 | |
| 177 | button.session-tab { |
| 178 | position: relative; |
| 179 | margin-inline: calc(-0.5 * var(--tab-slot-overlap)); |
| 180 | flex: 0 1 auto; min-width: 0; max-width: calc(10rem + 2 * var(--tab-foot)); |
| 181 | display: inline-flex; align-items: center; gap: 5px; |
| 182 | padding: 3px 8px 4px; margin-bottom: 4px; |
| 183 | border: 0 solid transparent; border-width: 0 var(--tab-foot); |
| 184 | border-radius: var(--tab-top) var(--tab-top) 0 0; |
| 185 | background: transparent; color: var(--fg-muted); |
| 186 | font: inherit; font-size: 11px; line-height: 1.4; |
| 187 | white-space: nowrap; cursor: pointer; text-align: left; |
| 188 | } |
| 189 | button.session-tab .name { overflow: hidden; text-overflow: ellipsis; min-width: 0; } |
| 190 | button.session-tab:hover { background: rgba(128, 128, 128, 0.16); color: var(--fg); } |
| 191 | button.session-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } |
| 192 | /* Attached by some other client: a terminal somewhere is on this session too. */ |
| 193 | button.session-tab.attached { color: var(--fg); } |
| 194 | /* The selected session is drawn in the window row's own surface and reaches |
| 195 | down to meet it, so the windows below read as this session's contents. */ |
| 196 | button.session-tab[aria-selected="true"] { |
| 197 | --surface: var(--bg-row); |
| 198 | background: var(--surface); color: var(--fg); |
| 199 | margin-bottom: 0; padding-bottom: 8px; |
| 200 | cursor: default; |
| 201 | } |
| 202 | button.session-tab[aria-selected="true"]:hover { background: var(--surface); } |
| 203 | button.session-tab[aria-selected="true"] .name { font-weight: 600; } |
| 204 | |
| 205 | /* --- window tabs ----------------------------------------------------------- |
| 206 | A tab is a window of the attached session — what `prefix 2` selects. Clicking |
| 207 | one is a select-window: nothing in this strip spawns or destroys a shell |
| 208 | except "+", which opens a window. */ |
| 209 | /* Sized to its tabs, not to the row: "+" sits against the last tab the way a |
| 210 | browser's does, and the strip gives up width to scrolling before the row |
| 211 | gives up its buttons. */ |
| 212 | /* The strip has to reach over the header's bottom border, because the active |
| 213 | tab's whole trick is covering the 1px of it that it sits on. `overflow-x: |
| 214 | auto` computes overflow-y to `auto` as well, so a scroll container clips its |
| 215 | children at the padding box no matter which axis you asked for: without the |
| 216 | margin/padding pair below, the tab's overhang is cut off at the seam and the |
| 217 | line runs straight through the one tab that is meant to break it. |
| 218 | The margin grows the box downwards onto the border, the padding puts the |
| 219 | tabs' own bottom edge back where it was, and the 1px between the two is room |
| 220 | the overhang is allowed to paint in. */ |
| 221 | #tabs { |
| 222 | display: flex; align-items: stretch; gap: 0; |
| 223 | flex: 0 1 auto; min-width: 0; |
| 224 | margin-bottom: -1px; padding-bottom: 1px; |
| 225 | overflow-x: auto; scrollbar-width: none; |
| 226 | } |
| 227 | #tabs::-webkit-scrollbar { display: none; } |
| 228 | /* Nothing follows the tabs, so this is just what stops "+" from being stretched |
| 229 | away from the last one. */ |
| 230 | #tab-new { margin-right: auto; } |
| 231 | |
| 232 | /* No extra separation between the last control and the tab strip: the strip's |
| 233 | own edge padding — the room the first tab's foot needs — already reads as the |
| 234 | gap between the two groups, and adding to it pushes the tabs visibly away |
| 235 | from the button for no reason. */ |
| 236 | |
| 237 | /* The tab and its close ✕ are siblings — a button cannot contain a button — |
| 238 | so the slot is what the strip lays out, and the ✕ is positioned over the |
| 239 | tab's right edge. |
| 240 | |
| 241 | Past the min width a name is unreadable, so the strip scrolls instead of |
| 242 | shrinking further: the same trade browsers make when the bar fills up. */ |
| 243 | /* Tabs overlap, as Chromium's do: it lays tab n+1's edge `2R - separator_width` |
| 244 | left of tab n's, so the slot left between two bodies is exactly the width the |
| 245 | separator is centred in. Ours is 6px, from 20px of feet less 14px of overlap. |
| 246 | The outermost edges stay flush, or the row would clip the first tab's foot. */ |
| 247 | .tab-slot { |
| 248 | position: relative; display: flex; align-items: stretch; |
| 249 | margin-inline: calc(-0.5 * var(--tab-slot-overlap)); |
| 250 | flex: 0 1 auto; |
| 251 | min-width: calc(5.5rem + 2 * var(--tab-foot)); |
| 252 | max-width: calc(11rem + 2 * var(--tab-foot)); |
| 253 | } |
| 254 | /* The window you are on gives up its characters last. */ |
| 255 | .tab-slot.active { min-width: calc(7rem + 2 * var(--tab-foot)); } |
| 256 | |
| 257 | /* Room for the ✕ is reserved rather than made on hover, so a name does not |
| 258 | re-ellipsis under the pointer. */ |
| 259 | .tab-slot.closable button.tab { padding-right: 20px; } |
| 260 | |
| 261 | /* Pinned: down to the dot and the index, sized to its content and sitting at |
| 262 | the head of the strip. The index is what stays visible rather than the name, |
| 263 | because the index is the thing you can type — `prefix 3` still selects it, |
| 264 | pinned or not. */ |
| 265 | .tab-slot.pinned { flex: 0 0 auto; min-width: 0; max-width: none; } |
| 266 | .tab-slot.pinned button.tab { padding-left: 7px; padding-right: 7px; gap: 4px; } |
| 267 | .tab-slot.pinned button.tab .index { opacity: 1; font-size: 11px; } |
| 268 | /* A hairline against the unpinned run, the way a browser separates them. */ |
| 269 | .tab-slot.pinned + .tab-slot:not(.pinned) { margin-left: 5px; } |
| 270 | .tab-slot.pinned + .tab-slot:not(.pinned)::before { |
| 271 | content: ""; position: absolute; left: -3px; top: 6px; bottom: 6px; |
| 272 | border-left: 1px solid var(--border); |
| 273 | } |
| 274 | |
| 275 | button.tab { |
| 276 | position: relative; |
| 277 | flex: 1 1 auto; min-width: 0; |
| 278 | display: inline-flex; align-items: center; gap: 5px; |
| 279 | padding: 4px 8px 5px; margin-bottom: 4px; |
| 280 | border: 0 solid transparent; border-width: 0 var(--tab-foot); |
| 281 | border-radius: var(--tab-top) var(--tab-top) 0 0; |
| 282 | background: transparent; color: var(--fg-muted); |
| 283 | font: inherit; font-size: 11px; line-height: 1.4; |
| 284 | white-space: nowrap; cursor: pointer; text-align: left; |
| 285 | } |
| 286 | button.tab:hover { background: rgba(128, 128, 128, 0.16); color: var(--fg); } |
| 287 | button.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } |
| 288 | button.tab .name { overflow: hidden; text-overflow: ellipsis; min-width: 0; } |
| 289 | /* Only a pinned tab carries one — it is what a tab that has given up its name |
| 290 | has left to be told apart by. An unpinned tab shows the name instead: in a |
| 291 | sidebar the number was spending room on something the tooltip and tmux's own |
| 292 | status line both already say. */ |
| 293 | button.tab .index { |
| 294 | font-size: 10.5px; opacity: .75; font-variant-numeric: tabular-nums; flex: none; |
| 295 | } |
| 296 | /* tmux's own "something happened here while you were away". */ |
| 297 | button.tab.activity { color: var(--fg); } |
| 298 | button.tab.activity .name { font-weight: 600; } |
| 299 | |
| 300 | /* The favicon slot: what Claude Code is doing in that window, and nothing at |
| 301 | all for a window that is just a shell — an always-lit glyph would be a status |
| 302 | light reporting on nothing. |
| 303 | |
| 304 | The glyph is Claude Code's own asterisk spinner, so a working window in the |
| 305 | strip and the transcript in the pane are saying the same thing the same way. |
| 306 | The box is sized and centred once, for the widest frame, so the name beside |
| 307 | it does not step sideways as the animation cycles — the frames come from |
| 308 | whatever font the platform falls back to for each one, and their advance |
| 309 | widths differ by several pixels. |
| 310 | |
| 311 | An *unselected* session tab wears the same glyph for the loudest agent |
| 312 | anywhere in the session, so one rule covers both rows: a waiting Claude looks |
| 313 | the same whether you can see its window or only the session that holds it. |
| 314 | The selected tab has no glyph to style — the windows it summarises are the |
| 315 | row underneath, already saying it one at a time. */ |
| 316 | button.tab .glyph, |
| 317 | button.session-tab .glyph { |
| 318 | flex: none; |
| 319 | width: 10px; height: 11px; |
| 320 | display: inline-flex; align-items: center; justify-content: center; |
| 321 | font-size: 10px; line-height: 1; |
| 322 | color: var(--fg-muted); opacity: .5; |
| 323 | } |
| 324 | button.tab .glyph.none, |
| 325 | button.session-tab .glyph.none { display: none; } |
| 326 | button.tab .glyph.working, |
| 327 | button.session-tab .glyph.working { color: var(--warn); opacity: 1; } |
| 328 | button.tab .glyph.waiting, |
| 329 | button.session-tab .glyph.waiting { color: var(--accent-hover); opacity: 1; animation: pulse 1.4s ease-in-out infinite; } |
| 330 | /* Claude has stopped with something for you in a window you have not been |
| 331 | shown since. Same glyph as idle, lit: grey is "nothing to do here", and the |
| 332 | only thing that changed is that there now is. Slower than the waiting pulse |
| 333 | and a different hue, because it is the softer of the two asks — nothing is |
| 334 | blocked on it, it is just done. */ |
| 335 | button.tab .glyph.ready, |
| 336 | button.session-tab .glyph.ready { color: var(--ok); opacity: 1; animation: pulse 2.2s ease-in-out infinite; } |
| 337 | |
| 338 | /* The lift: the terminal's background, and a negative bottom margin that |
| 339 | punches through the header's seam so the tab and the terminal read as one |
| 340 | surface. */ |
| 341 | /* Clicking the tab you are already on does nothing, so it does not offer the |
| 342 | hand — the same thing a browser's own active tab does. The ✕ is a sibling |
| 343 | button and keeps its own pointer. */ |
| 344 | button.tab[aria-selected="true"] { |
| 345 | --surface: var(--bg); |
| 346 | background: var(--surface); color: var(--fg); |
| 347 | margin-bottom: -1px; padding-bottom: 10px; |
| 348 | cursor: default; |
| 349 | } |
| 350 | button.tab[aria-selected="true"]:hover { background: var(--surface); } |
| 351 | |
| 352 | /* --- the tab silhouette ---------------------------------------------------- |
| 353 | A browser tab is not a rounded rectangle. Chromium walks the outline in |
| 354 | TabStyleViews::GetPath: up from the floor through a *concave* quarter circle |
| 355 | (the endcap, radius R), straight up the body's side, over a *convex* top |
| 356 | corner (radius r), across, and back down the mirror image. Two consequences |
| 357 | worth naming, because they are what makes the shape read as a tab: |
| 358 | |
| 359 | - the body is inset from the tab's own width by R on each side, so the top |
| 360 | is narrower than the bottom (`tab_left = left + extension`, where the |
| 361 | extension is exactly the bottom corner radius); |
| 362 | - each foot is tangent to both the floor and the body's side — its arc is |
| 363 | centred on the body's edge line, R above the floor — so the tab leaves |
| 364 | the floor horizontally rather than meeting it at a corner. |
| 365 | |
| 366 | Drawn as one additive mask over the tab's own box, which the transparent |
| 367 | side borders above make R wider than the body on each side. Not as |
| 368 | pseudo-elements hanging past the tab's edges: the rows are scroll containers |
| 369 | and clip at their padding box, so hanging feet only survive if the row |
| 370 | reserves a foot's width before its first tab, and that reserve is dead space |
| 371 | in the row whenever that tab isn't the selected one. |
| 372 | |
| 373 | Six layers, every one of them overlapping its neighbours, all unioned. An |
| 374 | earlier version tiled layers edge to edge and intersected them; the joins |
| 375 | landed on fractional device pixels and the surface behind the tab showed |
| 376 | through as hairlines, right where the tab is meant to be merging with it. |
| 377 | Overlapping unions have no join to miss. The half-pixel colour stops are |
| 378 | there for the same reason: a hard stop gives an unantialiased, stair-stepped |
| 379 | arc. */ |
| 380 | button.session-tab[aria-selected="true"], |
| 381 | button.tab[aria-selected="true"] { |
| 382 | /* The flare: an R square of surface with a quarter disc bitten out of its |
| 383 | outer top corner, which is the concave arc. 1px wider than the foot so it |
| 384 | laps under the body rather than abutting it. */ |
| 385 | --foot-l: radial-gradient(circle var(--tab-foot) at top left, |
| 386 | #0000 calc(var(--tab-foot) - 0.5px), #000 calc(var(--tab-foot) + 0.5px)); |
| 387 | --foot-r: radial-gradient(circle var(--tab-foot) at top right, |
| 388 | #0000 calc(var(--tab-foot) - 0.5px), #000 calc(var(--tab-foot) + 0.5px)); |
| 389 | --fill: linear-gradient(#000, #000); |
| 390 | /* The body's rounded shoulders, as discs the straight parts lap onto. */ |
| 391 | --top-l: radial-gradient(circle var(--tab-top) at |
| 392 | calc(var(--tab-foot) + var(--tab-top)) var(--tab-top), |
| 393 | #000 calc(var(--tab-top) - 0.5px), #0000 calc(var(--tab-top) + 0.5px)); |
| 394 | --top-r: radial-gradient(circle var(--tab-top) at |
| 395 | calc(100% - var(--tab-foot) - var(--tab-top)) var(--tab-top), |
| 396 | #000 calc(var(--tab-top) - 0.5px), #0000 calc(var(--tab-top) + 0.5px)); |
| 397 | --mask-layers: |
| 398 | var(--foot-l) left bottom / calc(var(--tab-foot) + 1px) var(--tab-foot) no-repeat, |
| 399 | var(--foot-r) right bottom / calc(var(--tab-foot) + 1px) var(--tab-foot) no-repeat, |
| 400 | var(--top-l) 0 0 / 100% 100% no-repeat, |
| 401 | var(--top-r) 0 0 / 100% 100% no-repeat, |
| 402 | /* the body below its shoulders, and the strip between them */ |
| 403 | var(--fill) center bottom / calc(100% - 2 * var(--tab-foot)) |
| 404 | calc(100% - var(--tab-top)) no-repeat, |
| 405 | var(--fill) center top / |
| 406 | calc(100% - 2 * var(--tab-foot) - 2 * var(--tab-top)) 100% no-repeat; |
| 407 | -webkit-mask: var(--mask-layers); |
| 408 | mask: var(--mask-layers); |
| 409 | } |
| 410 | |
| 411 | /* --- separators ------------------------------------------------------------ |
| 412 | Chromium draws a 2x16 pill (radius 1, so it is a stadium and not a hairline) |
| 413 | centred in the slot between two tab bodies, and hides it whenever either |
| 414 | neighbour is active, selected or hovered — `has_visible_background` in |
| 415 | TabStyleViews::GetSeparatorOpacity — crossfading over the hover animation so |
| 416 | that scrubbing the pointer along the row doesn't flicker. |
| 417 | |
| 418 | One per boundary, drawn on the right-hand tab of each pair: Chromium computes |
| 419 | it on both facing tabs and relies on them landing on the same coordinate, |
| 420 | which in CSS would just double-paint. Absolutely positioned, never a border, |
| 421 | or hovering a tab would shift every tab after it by a pixel. |
| 422 | |
| 423 | The colour is --border rather than Chromium's "sliver of the toolbar showing |
| 424 | through": that trick needs the surface below to contrast with the strip, and |
| 425 | in the dark theme ours are two units apart. */ |
| 426 | .tab-slot + .tab-slot > button.tab::before, |
| 427 | button.session-tab + button.session-tab::before, |
| 428 | #tab-new::before, |
| 429 | #session-new::before { |
| 430 | content: ""; |
| 431 | position: absolute; top: 50%; translate: 0 -50%; |
| 432 | /* Chromium's is 2 DIP on a 34 DIP tab; ours is a hairline on a 26px one, |
| 433 | which at this size reads as a divider rather than as a bar. */ |
| 434 | width: 1px; height: 11px; |
| 435 | border-radius: 1px; |
| 436 | background: var(--border); |
| 437 | pointer-events: none; |
| 438 | transition: opacity 160ms ease-out; |
| 439 | } |
| 440 | /* Centred in the lane between the two bodies. Offsets on an absolutely |
| 441 | positioned child are measured from its parent's *padding* box, which here |
| 442 | starts a whole foot inside the tab's own edge — so this counts left from the |
| 443 | body's leading edge rather than right from the tab's. */ |
| 444 | .tab-slot + .tab-slot > button.tab::before, |
| 445 | button.session-tab + button.session-tab::before { |
| 446 | left: calc(-0.5 * var(--tab-gap) - 0.5px); |
| 447 | } |
| 448 | /* The trailing one, between the last tab and "+", is the same boundary seen |
| 449 | from the other side. Chromium keeps this one and drops the leading one |
| 450 | outright — `leading ? 0.0f` in GetSeparatorOpacity — so there is nothing |
| 451 | between the controls ahead of the strip and the first tab. */ |
| 452 | /* Between the last body's edge (a foot to the button's left) and the button's |
| 453 | own glyph (about 5.5px inside it) — centred in what the eye sees as the gap, |
| 454 | which is wider here than the lane between two tabs. */ |
| 455 | #tab-new::before, #session-new::before { left: -2.5px; } |
| 456 | |
| 457 | /* Hidden next to anything with a background of its own: the tab itself, and |
| 458 | the boundary on its far side. */ |
| 459 | .tab-slot.active > button.tab::before, |
| 460 | .tab-slot:hover > button.tab::before, |
| 461 | .tab-slot.active + .tab-slot > button.tab::before, |
| 462 | .tab-slot:hover + .tab-slot > button.tab::before, |
| 463 | button.session-tab[aria-selected="true"]::before, |
| 464 | button.session-tab:hover::before, |
| 465 | button.session-tab[aria-selected="true"] + button.session-tab::before, |
| 466 | button.session-tab:hover + button.session-tab::before { opacity: 0; } |
| 467 | /* Same rule for the "+" boundary, which needs :has() to look backwards. */ |
| 468 | #tabs:has(.tab-slot:last-child:is(.active, :hover)) ~ #tab-new::before, |
| 469 | #sessions:has(button.session-tab:last-child:is([aria-selected="true"], :hover)) ~ #session-new::before { |
| 470 | opacity: 0; |
| 471 | } |
| 472 | /* The selected tab paints over its neighbours, feet included. */ |
| 473 | .tab-slot.active, button.session-tab[aria-selected="true"] { z-index: 1; } |
| 474 | /* The "+" buttons carry a separator, so they contain it. */ |
| 475 | #tab-new, #session-new { position: relative; } |
| 476 | |
| 477 | /* The overlap is between tabs, not around the run of them: the outermost edges |
| 478 | pull back to flush, or half a foot hangs outside the scroll container and is |
| 479 | clipped away — the row's ends are exactly where a foot needs its room. */ |
| 480 | #tabs > .tab-slot:first-child, |
| 481 | #sessions > button.session-tab:first-child { margin-left: 0; } |
| 482 | #tabs > .tab-slot:last-child, |
| 483 | #sessions > button.session-tab:last-child { margin-right: 0; } |
| 484 | |
| 485 | /* The run as a whole sits a little further left than the control before it, |
| 486 | so the first tab's foot laps over that button's padding rather than leaving |
| 487 | a notch beside it. On the run and not on the first tab: pulling the tab |
| 488 | alone would take its outer foot past the scroll container's edge, and the |
| 489 | container would clip exactly the part of the curve this is about. */ |
| 490 | #tabs, #sessions { margin-left: -4px; } |
| 491 | |
| 492 | /* Being dragged: the tab keeps its place in the row (it is the element being |
| 493 | moved through it), so the only cue needed is that it is the one in hand. */ |
| 494 | .session-tab.dragging, .tab-slot.dragging { opacity: .45; } |
| 495 | .tab-slot.dragging button.tab-close { opacity: 0; pointer-events: none; } |
| 496 | |
| 497 | /* --- tab groups ------------------------------------------------------------ |
| 498 | The single-row layout. One strip holds every window on the server, and each |
| 499 | session's run of them is introduced by a chip. |
| 500 | |
| 501 | With the session row gone there are two surfaces rather than three, so the |
| 502 | remaining one takes the deepest of them and the selected tab still lifts all |
| 503 | the way to the terminal — the same gesture, one level shallower. */ |
| 504 | body.groups #window-strip { background: var(--bg-strip); } |
| 505 | /* Two "+"s in one row now — a window in the session we are on, then a session. |
| 506 | Only the trailing one takes the row's leftover width, or the pair would be |
| 507 | pushed apart by it and read as belonging to different things. */ |
| 508 | body.groups #tab-new { margin-right: 0; } |
| 509 | /* The run starts with a chip, which has no foot to lap over the control before |
| 510 | it, so it does not get the pull the tabs need. */ |
| 511 | body.groups #tabs { margin-left: 0; } |
| 512 | |
| 513 | /* The chip is not a tab and is deliberately not shaped like one: no feet, no |
| 514 | top corners, nothing that reaches the strip's bottom edge. It is a label with |
| 515 | a fold, sitting on the row rather than rising out of it, which is what keeps |
| 516 | a row of mixed chips and tabs readable as "groups of tabs" rather than as two |
| 517 | kinds of tab. |
| 518 | |
| 519 | --group-h is set per chip in JS, hashed from the session name: one hue for |
| 520 | that session everywhere, with nothing stored and nothing to assign. Both the |
| 521 | text and the fill are mixed *towards the theme's own colours* rather than |
| 522 | given fixed lightnesses, so one rule covers light and dark — the same hue |
| 523 | darkens on white and lightens on the dark strip. */ |
| 524 | .group-chip { |
| 525 | --group-c: hsl(var(--group-h, 210) 60% 50%); |
| 526 | position: relative; |
| 527 | flex: none; |
| 528 | align-self: center; |
| 529 | display: inline-flex; align-items: center; gap: 4px; |
| 530 | margin: 0 2px 4px 8px; |
| 531 | padding: 2px 7px 3px; |
| 532 | border: 0; border-radius: 9px; |
| 533 | background: color-mix(in srgb, var(--group-c) 15%, transparent); |
| 534 | color: color-mix(in srgb, var(--group-c) 60%, var(--fg)); |
| 535 | font: inherit; font-size: 10.5px; line-height: 1.4; font-weight: 600; |
| 536 | white-space: nowrap; cursor: pointer; |
| 537 | } |
| 538 | /* Nothing precedes the first chip, so it takes the same flush edge the first |
| 539 | tab does. */ |
| 540 | #tabs > .group-chip:first-child { margin-left: 0; } |
| 541 | /* Grey: the home session's default, and a colour you can pick outright. |
| 542 | Overriding the one custom property is enough; every rule below is written |
| 543 | against it. */ |
| 544 | .group-chip.grey { --group-c: var(--fg-muted); } |
| 545 | .group-chip:hover { background: color-mix(in srgb, var(--group-c) 26%, transparent); } |
| 546 | .group-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 547 | /* The group this panel's client is in. A ring rather than more fill: the fill |
| 548 | is already carrying the identity, and a second use of it would say "louder |
| 549 | session" instead of "you are here". */ |
| 550 | .group-chip.current { |
| 551 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--group-c) 70%, transparent); |
| 552 | color: color-mix(in srgb, var(--group-c) 45%, var(--fg)); |
| 553 | } |
| 554 | /* Some other tmux client is on this session — the same thing the nested |
| 555 | layout's session tab says by going full-strength. */ |
| 556 | .group-chip.attached .name { text-decoration: underline; text-underline-offset: 2px; } |
| 557 | /* Mid-drag, with a window hovering over it: the drop moves that window into |
| 558 | this session. */ |
| 559 | .group-chip.drop { |
| 560 | background: color-mix(in srgb, var(--group-c) 45%, transparent); |
| 561 | color: var(--fg); |
| 562 | } |
| 563 | |
| 564 | /* A triangle rather than a chevron glyph: at 4px a font's chevron is at the |
| 565 | mercy of whichever fallback the platform picks, and this one is drawn. */ |
| 566 | .group-chip .caret { |
| 567 | flex: none; |
| 568 | width: 0; height: 0; |
| 569 | border: 3.5px solid transparent; |
| 570 | border-top: 4px solid currentColor; |
| 571 | /* Borders build the triangle from the box's centre, so the bottom half of it |
| 572 | is empty space; this puts the visible half back on the text's midline. */ |
| 573 | margin-top: 2px; |
| 574 | } |
| 575 | .group-chip.folded .caret { |
| 576 | border: 3.5px solid transparent; |
| 577 | border-left: 4px solid currentColor; |
| 578 | margin-top: 0; margin-left: 1px; |
| 579 | } |
| 580 | .group-chip .count { |
| 581 | font-size: 9.5px; opacity: .7; font-variant-numeric: tabular-nums; flex: none; |
| 582 | font-weight: 400; |
| 583 | } |
| 584 | /* A folded group's windows have no tabs to wear their status, so the chip wears |
| 585 | the loudest of them. Same sizes as a tab's favicon slot. */ |
| 586 | .group-chip .glyph { |
| 587 | flex: none; |
| 588 | width: 10px; height: 11px; |
| 589 | display: inline-flex; align-items: center; justify-content: center; |
| 590 | font-size: 10px; line-height: 1; |
| 591 | opacity: .8; |
| 592 | } |
| 593 | .group-chip .glyph.working { color: var(--warn); opacity: 1; } |
| 594 | .group-chip .glyph.waiting { color: var(--accent-hover); opacity: 1; animation: pulse 1.4s ease-in-out infinite; } |
| 595 | .group-chip .glyph.ready { color: var(--ok); opacity: 1; animation: pulse 2.2s ease-in-out infinite; } |
| 596 | |
| 597 | /* The run's ends are flush against the chips either side of it, for the same |
| 598 | reason the strip's own ends are: half a foot hanging under a chip reads as a |
| 599 | tab starting behind it. */ |
| 600 | #tabs > .group-chip + .tab-slot { margin-left: 0; } |
| 601 | #tabs > .tab-slot:has(+ .group-chip) { margin-right: 0; } |
| 602 | |
| 603 | /* The current window of a session this panel is *not* on. tmux marks one per |
| 604 | session, and in this row they are all visible at once — worth saying, but it |
| 605 | is not the tab you are looking at and must not be drawn as one. */ |
| 606 | .tab-slot.elsewhere > button.tab { color: var(--fg); } |
| 607 | .tab-slot.elsewhere > button.tab .name { font-weight: 600; } |
| 608 | |
| 609 | /* --- the omnibar ----------------------------------------------------------- |
| 610 | Groups mode's second row: a browser's address bar, under the tabs, in the |
| 611 | same place and doing the same job — search what you have, or create what you |
| 612 | named. The picker and the jump button lead it, in that order, and Enter and |
| 613 | push-to-talk end it — the same pair, in the same order, that leads the window |
| 614 | row in the nested layout. */ |
| 615 | #omni-strip { |
| 616 | background: var(--bg); |
| 617 | padding: 3px 6px 4px 4px; |
| 618 | align-items: center; |
| 619 | gap: 2px; |
| 620 | } |
| 621 | /* The pill is the wrapper, not the input: the location sits inside it, so the |
| 622 | border and the focus ring have to go around both. */ |
| 623 | #omni-box { |
| 624 | flex: 1 1 auto; min-width: 0; |
| 625 | display: flex; align-items: center; gap: 6px; |
| 626 | /* Nothing inside may paint past the pill: at the narrowest the directory is |
| 627 | capped rather than shrunk, so it can want a few pixels more than are left. */ |
| 628 | overflow: hidden; |
| 629 | height: 24px; |
| 630 | padding: 0 9px; |
| 631 | border: 1px solid var(--border); border-radius: 12px; |
| 632 | background: var(--bg-input); |
| 633 | } |
| 634 | #omni-box:focus-within { |
| 635 | border-color: var(--accent); |
| 636 | /* The ring is the border thickening inwards, so the row's height does not |
| 637 | change when the box takes focus. */ |
| 638 | box-shadow: inset 0 0 0 1px var(--accent); |
| 639 | } |
| 640 | #omni { |
| 641 | /* Enough left for a few characters of the name and the caret: the directory |
| 642 | beside it shrinks first, because a session you cannot read the name of is |
| 643 | worse than a path missing its front. */ |
| 644 | flex: 1 1 auto; min-width: 4rem; |
| 645 | height: 100%; |
| 646 | font: inherit; font-size: 11px; |
| 647 | padding: 0; border: 0; background: none; color: var(--fg); |
| 648 | } |
| 649 | #omni:focus, #omni:focus-visible { outline: none; } |
| 650 | #omni::placeholder { color: var(--fg-muted); } |
| 651 | |
| 652 | /* The directory, at the far end of the box. Dimmed and never wider than half |
| 653 | of it: it is context for the name, not a second thing competing with it, and |
| 654 | a deep path must not push the name it belongs to out of view. Read from its |
| 655 | end like every path here, so what goes when it does not fit is the front. */ |
| 656 | #omni-cwd { |
| 657 | /* It neither grows nor shrinks: a text input's flex base size is far wider |
| 658 | than the text in it, so a shrinkable directory beside one is squeezed |
| 659 | against a phantom — the box reads as half empty and the path still ends in |
| 660 | an ellipsis. Fixed at its content width, the input absorbs the slack |
| 661 | instead, and the only thing that can cut the path is the cap. */ |
| 662 | flex: 0 0 auto; min-width: 0; max-width: 72%; |
| 663 | font-size: 10px; |
| 664 | color: var(--fg-muted); |
| 665 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 666 | direction: rtl; text-align: right; |
| 667 | } |
| 668 | #omni-cwd > span { direction: ltr; unicode-bidi: embed; } |
| 669 | |
| 670 | /* The slot a browser keeps its site-info button in, doing the same job for the |
| 671 | session. The hit area is a finger's worth of button and the glyph inside it |
| 672 | stays small — muted until you go for it, the way a chrome control that is not |
| 673 | asking for anything should sit. */ |
| 674 | #omni-here { |
| 675 | flex: none; |
| 676 | display: flex; align-items: center; justify-content: center; |
| 677 | width: 18px; height: 18px; margin-left: -4px; |
| 678 | padding: 0; border: 0; border-radius: 50%; |
| 679 | background: none; color: var(--fg-muted); cursor: pointer; |
| 680 | } |
| 681 | #omni-here svg { width: 13px; height: 13px; display: block; } |
| 682 | #omni-here:hover { background: var(--bg-panel); color: var(--fg); } |
| 683 | #omni-here[aria-expanded="true"] { background: var(--bg-panel); color: var(--fg); } |
| 684 | #omni-here:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; } |
| 685 | |
| 686 | /* --- session info ---------------------------------------------------------- |
| 687 | The panel behind the dot. A browser's site-info bubble in the same place and |
| 688 | with the same shape: rows of label and value, the ones you might want to |
| 689 | carry elsewhere copyable, and nothing modal about it. */ |
| 690 | .info-pop { |
| 691 | position: fixed; z-index: 20; |
| 692 | min-width: 15rem; max-width: min(22rem, calc(100vw - 12px)); |
| 693 | display: flex; flex-direction: column; gap: 5px; |
| 694 | padding: 8px 10px; |
| 695 | font-size: 11px; line-height: 1.45; |
| 696 | background: var(--bg-panel); color: var(--fg); |
| 697 | border: 1px solid var(--border); border-radius: 8px; |
| 698 | box-shadow: 0 6px 20px var(--shadow); |
| 699 | } |
| 700 | .info-pop .info-head { |
| 701 | display: flex; align-items: center; gap: 6px; |
| 702 | padding-bottom: 5px; |
| 703 | border-bottom: 1px solid var(--border); |
| 704 | } |
| 705 | .info-pop .info-head .dot { |
| 706 | flex: none; width: 8px; height: 8px; border-radius: 50%; |
| 707 | background: hsl(var(--group-h, 210) 60% 50%); |
| 708 | } |
| 709 | .info-pop .info-head .dot.grey { background: var(--fg-muted); } |
| 710 | .info-pop .info-head .name { |
| 711 | font-weight: 600; |
| 712 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 713 | } |
| 714 | .info-pop .info-head .sub { color: var(--fg-muted); font-size: 10px; } |
| 715 | .info-row { display: flex; align-items: baseline; gap: 8px; } |
| 716 | .info-row .k { flex: none; width: 4.5rem; color: var(--fg-muted); } |
| 717 | .info-row .v { |
| 718 | flex: 1 1 auto; min-width: 0; |
| 719 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 720 | } |
| 721 | /* A path is read from its end — the leaf is the part that identifies it — so |
| 722 | when it does not fit, the front is what goes. */ |
| 723 | .info-row .v.path { |
| 724 | font-family: ui-monospace, monospace; direction: rtl; text-align: left; |
| 725 | } |
| 726 | .info-row .v.path > span { direction: ltr; unicode-bidi: embed; } |
| 727 | .info-pop .info-agents { |
| 728 | display: flex; flex-direction: column; gap: 3px; |
| 729 | padding-top: 5px; border-top: 1px solid var(--border); |
| 730 | } |
| 731 | .info-pop .info-agent { display: flex; align-items: baseline; gap: 6px; } |
| 732 | .info-pop .info-agent .glyph { |
| 733 | flex: none; width: 10px; text-align: center; |
| 734 | color: var(--fg-muted); opacity: .6; |
| 735 | } |
| 736 | .info-pop .info-agent .glyph.working { color: var(--warn); opacity: 1; } |
| 737 | .info-pop .info-agent .glyph.waiting { color: var(--accent-hover); opacity: 1; } |
| 738 | /* No pulse in the panel or the omnibox: both are lists you are already reading |
| 739 | top to bottom, and a light that breathes in a list you are scanning is the |
| 740 | kind of motion the strip earns by being glanced at and these do not. */ |
| 741 | .info-pop .info-agent .glyph.ready { color: var(--ok); opacity: 1; } |
| 742 | .info-pop .info-agent .what { |
| 743 | flex: 1 1 auto; min-width: 0; |
| 744 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 745 | } |
| 746 | .info-pop .info-agent .where { flex: none; color: var(--fg-muted); font-size: 10px; } |
| 747 | .info-pop .info-empty { color: var(--fg-muted); } |
| 748 | .info-pop .info-copy { |
| 749 | align-self: flex-start; |
| 750 | padding: 2px 6px; |
| 751 | font: inherit; font-size: 10px; |
| 752 | color: var(--fg-muted); background: none; |
| 753 | border: 1px solid var(--border); border-radius: 5px; |
| 754 | cursor: pointer; |
| 755 | } |
| 756 | .info-pop .info-copy:hover { color: var(--fg); border-color: var(--accent); } |
| 757 | |
| 758 | body.compact #omni-strip { padding: 2px 4px 3px 4px; } |
| 759 | body.compact #omni-box { height: 21px; } |
| 760 | |
| 761 | /* Over the terminal, not in the header: the header is a flex column that sizes |
| 762 | the terminal, so a list that grew inside it would reflow the terminal — and |
| 763 | re-fit xterm.js — on every keystroke. `top` is set in JS from the row's own |
| 764 | box, so it stays under the box at any density. */ |
| 765 | #omni-list { |
| 766 | position: fixed; left: 0; right: 0; |
| 767 | z-index: 5; |
| 768 | max-height: min(52vh, 320px); |
| 769 | overflow-y: auto; |
| 770 | padding: 3px 0; |
| 771 | background: var(--bg-panel); |
| 772 | border-bottom: 1px solid var(--border); |
| 773 | box-shadow: 0 6px 16px var(--shadow); |
| 774 | } |
| 775 | .omni-row { |
| 776 | display: flex; align-items: baseline; gap: 6px; |
| 777 | padding: 4px 10px; |
| 778 | font-size: 11px; line-height: 1.5; |
| 779 | color: var(--fg-muted); |
| 780 | cursor: pointer; |
| 781 | } |
| 782 | .omni-row .label { |
| 783 | color: var(--fg); |
| 784 | flex: 0 1 auto; min-width: 0; |
| 785 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 786 | } |
| 787 | /* The part you typed. The rest of the name is what tells you it is not the one |
| 788 | you meant, so the emphasis goes on the match rather than on the difference. */ |
| 789 | .omni-row .label b { color: var(--accent-hover); font-weight: 700; } |
| 790 | .omni-row .meta { |
| 791 | flex: 1 1 auto; min-width: 0; |
| 792 | text-align: right; |
| 793 | font-size: 10px; |
| 794 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 795 | } |
| 796 | /* Keyboard and pointer land on the same highlight — the row under the cursor |
| 797 | *is* the chosen one, so Enter and a click can never disagree. */ |
| 798 | .omni-row.active { background: var(--accent); } |
| 799 | .omni-row.active .label, |
| 800 | .omni-row.active .meta { color: var(--accent-fg); } |
| 801 | .omni-row.active .label b { color: var(--accent-fg); } |
| 802 | /* The one row that makes something rather than going somewhere. */ |
| 803 | .omni-row.create .label::before { |
| 804 | content: "+ "; |
| 805 | color: var(--ok); |
| 806 | font-weight: 700; |
| 807 | } |
| 808 | /* The row that reads the box as a question. Claude's own asterisk, the same |
| 809 | one the agent glyphs use, so the row is recognisable before it is read. */ |
| 810 | .omni-row.claude .label::before { |
| 811 | content: "✻ "; |
| 812 | color: var(--accent-hover); |
| 813 | font-weight: 700; |
| 814 | } |
| 815 | .omni-row.claude.active .label::before { color: var(--accent-fg); } |
| 816 | /* The row that reads the box as a shell command. A prompt's own mark, because |
| 817 | that is what the pane it opens will be showing. */ |
| 818 | .omni-row.run .label::before { |
| 819 | content: "$ "; |
| 820 | color: var(--ok); |
| 821 | font-weight: 700; |
| 822 | } |
| 823 | .omni-row.run .label { font-family: ui-monospace, monospace; } |
| 824 | /* The row that goes to another machine. An arrow leaving, because that is the |
| 825 | one thing about it that is not like every other row: the window it opens is |
| 826 | here, but what you end up talking to is somewhere else. */ |
| 827 | .omni-row.ssh .label::before { |
| 828 | content: "⇢ "; |
| 829 | color: var(--accent-hover); |
| 830 | font-weight: 700; |
| 831 | } |
| 832 | .omni-row.ssh.active .label::before { color: var(--accent-fg); } |
| 833 | .omni-row.ssh .label { font-family: ui-monospace, monospace; } |
| 834 | /* A directory, in both of the rows that are about one: what the box holds, and |
| 835 | the names it could be completed to. Monospaced for the reason the ssh and |
| 836 | shell rows are — a path is read character by character, and the column the |
| 837 | slashes line up in is how you read it. The completion's mark is a slash |
| 838 | because that is the key it stands in for. */ |
| 839 | .omni-row.project .label { font-family: ui-monospace, monospace; } |
| 840 | .omni-row.path .label { font-family: ui-monospace, monospace; } |
| 841 | .omni-row.path .label::before { |
| 842 | content: "/ "; |
| 843 | color: var(--fg-muted); |
| 844 | font-weight: 700; |
| 845 | } |
| 846 | .omni-row.path.active .label::before { color: var(--accent-fg); } |
| 847 | /* The rows that make something in this session rather than going anywhere. The |
| 848 | mark is per-row rather than per-kind — see `omniActions`. */ |
| 849 | .omni-row .mark { |
| 850 | flex: none; |
| 851 | color: var(--accent-hover); |
| 852 | font-weight: 700; |
| 853 | } |
| 854 | .omni-row.active .mark { color: var(--accent-fg); } |
| 855 | /* A row that is a label rather than an offer: the bare `!`, before there is a |
| 856 | command to run. Dimmed, and not styled as clickable, because it isn't. */ |
| 857 | .omni-row.hint { cursor: default; } |
| 858 | .omni-row.hint .label { color: var(--fg-muted); font-style: italic; } |
| 859 | .omni-row.run.active .label::before { color: var(--accent-fg); } |
| 860 | .omni-row .glyph { |
| 861 | flex: none; |
| 862 | width: 10px; height: 11px; |
| 863 | display: inline-flex; align-items: center; justify-content: center; |
| 864 | font-size: 10px; line-height: 1; |
| 865 | color: var(--fg-muted); opacity: .5; |
| 866 | align-self: center; |
| 867 | } |
| 868 | /* Unlike a tab's, this slot is always there even when it is empty: a list is |
| 869 | read down its left edge, and a glyph that collapses steps every row without |
| 870 | an agent out of line with the rows that have one. */ |
| 871 | .omni-row .glyph.working { color: var(--warn); opacity: 1; } |
| 872 | .omni-row .glyph.waiting { color: var(--accent-hover); opacity: 1; } |
| 873 | .omni-row .glyph.ready { color: var(--ok); opacity: 1; } |
| 874 | .omni-row.active .glyph { color: var(--accent-fg); opacity: 1; } |
| 875 | /* A finger is not a cursor: an 11px row is a comfortable read and a bad target, |
| 876 | so where the pointer is coarse the rows grow to something you can hit without |
| 877 | aiming. Nothing else about the list changes — the same rows, further apart. */ |
| 878 | @media (pointer: coarse) { |
| 879 | .omni-row { padding: 9px 10px; } |
| 880 | #omni-list { max-height: min(60vh, 420px); } |
| 881 | } |
| 882 | |
| 883 | /* Right-click menu. A plain positioned element: an extension page would |
| 884 | otherwise get the browser's own menu, which has nothing to say about tmux. */ |
| 885 | .tab-menu { |
| 886 | position: fixed; z-index: 20; min-width: 8rem; |
| 887 | display: flex; flex-direction: column; padding: 3px; |
| 888 | background: var(--bg-panel); color: var(--fg); |
| 889 | border: 1px solid var(--border); border-radius: 6px; |
| 890 | box-shadow: 0 6px 20px var(--shadow); |
| 891 | } |
| 892 | .tab-menu button { |
| 893 | background: transparent; color: var(--fg); |
| 894 | border: 0; border-radius: 4px; padding: 5px 8px; |
| 895 | font: inherit; font-size: 11px; text-align: left; cursor: pointer; |
| 896 | } |
| 897 | .tab-menu button:hover { background: var(--border); } |
| 898 | .tab-menu button.danger { color: var(--err); } |
| 899 | .tab-menu button.danger:hover { background: var(--err); color: var(--accent-fg); } |
| 900 | .tab-menu button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } |
| 901 | |
| 902 | /* The group's name, edited in place at the top of its menu — the same place a |
| 903 | browser puts a tab group's name. No separator under it: it and the palette |
| 904 | below are one block of "what this group is", divided from the actions. */ |
| 905 | .tab-menu .rename { |
| 906 | padding: 6px 8px 2px; |
| 907 | } |
| 908 | .tab-menu .rename input { |
| 909 | width: 100%; box-sizing: border-box; |
| 910 | padding: 4px 6px; |
| 911 | background: var(--bg); color: var(--fg); |
| 912 | border: 1px solid var(--border); border-radius: 4px; |
| 913 | font: inherit; font-size: 11px; |
| 914 | } |
| 915 | .tab-menu .rename input:focus { outline: none; border-color: var(--accent); } |
| 916 | /* A name tmux will not take. Said while it is being typed rather than after it |
| 917 | is sent, since the daemon's answer to one is silence. */ |
| 918 | .tab-menu .rename.bad input, |
| 919 | .tab-menu .rename.bad input:focus { border-color: var(--err); } |
| 920 | |
| 921 | /* The group palette, across the top of a group's menu. A row rather than nine |
| 922 | more menu items: these are a palette, and the eye finds a colour in a palette |
| 923 | faster than a colour name in a list. */ |
| 924 | .tab-menu .colours { |
| 925 | display: flex; gap: 5px; |
| 926 | padding: 7px 8px; |
| 927 | border-bottom: 1px solid var(--border); |
| 928 | margin-bottom: 3px; |
| 929 | } |
| 930 | .tab-menu .swatch { |
| 931 | flex: none; |
| 932 | width: 15px; height: 15px; padding: 0; |
| 933 | border-radius: 50%; border: 0; |
| 934 | background: hsl(var(--group-h, 210) 60% 50%); |
| 935 | cursor: pointer; |
| 936 | } |
| 937 | .tab-menu .swatch.grey { background: var(--fg-muted); } |
| 938 | .tab-menu .swatch:hover { transform: scale(1.15); } |
| 939 | /* The one showing now, ringed in the panel's own background so the ring reads |
| 940 | as a gap around the colour rather than as a second colour on top of it. */ |
| 941 | .tab-menu .swatch.on { |
| 942 | box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 3.5px var(--fg-muted); |
| 943 | } |
| 944 | .tab-menu .swatch:focus-visible { |
| 945 | outline: 2px solid var(--accent); outline-offset: 2px; |
| 946 | } |
| 947 | |
| 948 | button.tab-close { |
| 949 | position: absolute; right: calc(4px + var(--tab-foot)); top: 5px; |
| 950 | width: 15px; height: 15px; padding: 0; |
| 951 | display: inline-flex; align-items: center; justify-content: center; |
| 952 | border: 0; border-radius: 4px; |
| 953 | background: transparent; color: var(--fg-muted); cursor: pointer; |
| 954 | opacity: .45; transition: opacity 90ms linear; |
| 955 | } |
| 956 | button.tab-close svg { width: 9px; height: 9px; display: block; } |
| 957 | /* On every tab that has one, not just the active tab and the one under the |
| 958 | pointer: a ✕ you have to go looking for is a ✕ you do not know is there, and |
| 959 | the room for it is reserved either way — `.tab-slot.closable` pads for it |
| 960 | whether or not it is painted, so showing it costs no layout. |
| 961 | Dim until the tab is the one you are on or pointing at, which is what keeps a |
| 962 | full strip of them from reading as noise. Every one of them is destructive, |
| 963 | so the quiet default matters more here than in a browser. */ |
| 964 | .tab-slot.active button.tab-close, |
| 965 | .tab-slot:hover button.tab-close, |
| 966 | .tab-slot:focus-within button.tab-close { opacity: 1; } |
| 967 | /* Red on hover rather than the usual grey: this is the one control in the |
| 968 | panel that destroys something, and it takes effect on the first click. */ |
| 969 | button.tab-close:hover { background: var(--err); color: var(--accent-fg); } |
| 970 | button.tab-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; } |
| 971 | |
| 972 | |
| 973 | button.icon { |
| 974 | flex: none; |
| 975 | width: 26px; height: 26px; padding: 0; |
| 976 | display: inline-flex; align-items: center; justify-content: center; |
| 977 | background: transparent; border: 0; border-radius: 5px; |
| 978 | color: var(--fg-muted); cursor: pointer; |
| 979 | } |
| 980 | button.icon svg { width: 15px; height: 15px; display: block; } |
| 981 | button.icon:hover { background: var(--border); color: var(--fg); } |
| 982 | button.icon:active { transform: translateY(0.5px); } |
| 983 | button.icon:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; } |
| 984 | /* A window tab is hovering over "+", and letting go gives it a session of its |
| 985 | own. Lit rather than outlined, because the pointer is over the button and a |
| 986 | ring under a dragged tab reads as a gap. */ |
| 987 | button.icon.drop { |
| 988 | background: var(--accent); |
| 989 | color: var(--accent-fg); |
| 990 | } |
| 991 | #pick.active { background: var(--accent); color: var(--accent-fg); } |
| 992 | |
| 993 | /* Jump to the next pane an agent is waiting in. Tinted by the state it would |
| 994 | take you to, in the same three colours the tabs' glyphs use — the button is |
| 995 | the one place in the header that says what is loudest *anywhere* on the |
| 996 | server, so it has to answer "is there anything for me" before it is pressed |
| 997 | and without being read as a fourth vocabulary. |
| 998 | |
| 999 | No pulse: the tab whose glyph is pulsing is already saying that, and a second |
| 1000 | thing moving in the same row for the same reason is noise. Dimmed and |
| 1001 | unpressable when no pane has an agent in it — there is nowhere to go, and the |
| 1002 | button that goes nowhere should not look like it does. */ |
| 1003 | #jump[data-state="waiting"] { color: var(--accent-hover); } |
| 1004 | #jump[data-state="ready"] { color: var(--ok); } |
| 1005 | #jump[data-state="working"] { color: var(--warn); } |
| 1006 | button.icon:disabled { opacity: 0.4; cursor: default; } |
| 1007 | button.icon:disabled:hover { background: transparent; color: var(--fg-muted); } |
| 1008 | button.icon:disabled:active { transform: none; } |
| 1009 | |
| 1010 | /* Push to talk, while held. Red rather than the accent: this one is putting |
| 1011 | bytes into the pane for as long as it looks like this, and it needs to be |
| 1012 | obvious at a glance that it is still down — a key stuck open in a terminal is |
| 1013 | not a small mistake. The ring is what carries that at 26px, where a fill |
| 1014 | alone reads much like the picker's. |
| 1015 | |
| 1016 | `:active` is deliberately not the hook. The hold survives the pointer sliding |
| 1017 | off the button (it is captured), and :active does not. */ |
| 1018 | /* Much bigger than its neighbours, and round. Every other icon here is a tap, |
| 1019 | which forgives a near miss; this one is held for the length of a sentence, and |
| 1020 | sliding off it mid-word is the failure that matters. The size is the fix, and |
| 1021 | the circle is what says "hold" rather than "click" at a glance. |
| 1022 | |
| 1023 | It takes most of the corner, which is what the corner is for: a row can only |
| 1024 | ever give an icon its own 26px, and a thumb wants more than that. It is sized |
| 1025 | from the height it is given rather than from the corner's width, because the |
| 1026 | corner shares that height with the Enter key under it and the header is a row |
| 1027 | taller in one mode than the other — a circle fixed at the full width would |
| 1028 | push the header taller in the mode that cannot spare it. The floor is 40px |
| 1029 | and the ceiling 48; both platforms' touch guidance names 44 and the band |
| 1030 | straddles it. |
| 1031 | |
| 1032 | A lifted surface rather than the recessed one an input gets: this is the one |
| 1033 | control in the panel you aim a thumb at, and on the header's own background |
| 1034 | a well would read as a hole rather than a button. */ |
| 1035 | #talk.big { |
| 1036 | flex: 1 1 auto; |
| 1037 | width: auto; height: auto; |
| 1038 | min-height: 40px; max-height: 48px; |
| 1039 | aspect-ratio: 1; |
| 1040 | border-radius: 50%; |
| 1041 | border: 1px solid var(--border); |
| 1042 | background: var(--bg-panel); |
| 1043 | box-shadow: 0 1px 2px var(--shadow); |
| 1044 | } |
| 1045 | #talk.big:hover { |
| 1046 | background: color-mix(in srgb, var(--fg) 8%, var(--bg-panel)); |
| 1047 | color: var(--fg); |
| 1048 | } |
| 1049 | #talk.big svg { width: 21px; height: 21px; } |
| 1050 | |
| 1051 | #talk.talking { |
| 1052 | background: var(--err); |
| 1053 | color: var(--accent-fg); |
| 1054 | border-color: var(--err); |
| 1055 | box-shadow: 0 0 0 3px color-mix(in srgb, var(--err) 30%, transparent); |
| 1056 | } |
| 1057 | #talk.talking svg { animation: talk-pulse 1.1s ease-in-out infinite; } |
| 1058 | @keyframes talk-pulse { |
| 1059 | 50% { opacity: .45; } |
| 1060 | } |
| 1061 | @media (prefers-reduced-motion: reduce) { |
| 1062 | #talk.talking svg { animation: none; } |
| 1063 | } |
| 1064 | /* Let go and the Return is queued, not sent: for the length of that window the |
| 1065 | button says so, by fading the hold's own colour out over exactly as long as |
| 1066 | you have to press it again. Nothing is sent while the ring is visible, so the |
| 1067 | cue and the thing it reports on end together. The duration comes from JS so |
| 1068 | there is one number for it; the fallback matches TALK_SUBMIT_MS. */ |
| 1069 | #talk.pending { |
| 1070 | animation: talk-arm var(--talk-submit, 1s) linear forwards; |
| 1071 | } |
| 1072 | @keyframes talk-arm { |
| 1073 | from { box-shadow: 0 0 0 3px color-mix(in srgb, var(--err) 30%, transparent); } |
| 1074 | to { box-shadow: 0 0 0 3px transparent; } |
| 1075 | } |
| 1076 | @media (prefers-reduced-motion: reduce) { |
| 1077 | #talk.pending { animation: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--err) 15%, transparent); } |
| 1078 | } |
| 1079 | |
| 1080 | /* Held with a finger: the browser's own long-press behaviours — text selection, |
| 1081 | the callout menu — would otherwise interrupt the gesture. */ |
| 1082 | #talk { touch-action: none; -webkit-user-select: none; user-select: none; } |
| 1083 | |
| 1084 | /* The session name is a tab of its own once there is one, so the status text |
| 1085 | stands down rather than saying it twice. The dot stays: it is the only thing |
| 1086 | reporting on the connection itself. */ |
| 1087 | body.has-session > header #status-text { display: none; } |
| 1088 | |
| 1089 | /* Compact: lose the status text, keep the targets clickable. */ |
| 1090 | body.compact .strip { padding: 2px 4px 0; } |
| 1091 | body.compact #status-text { display: none; } |
| 1092 | body.compact button.icon { width: 24px; height: 24px; } |
| 1093 | /* The mic is a touch target rather than a glyph, so density trims it a little |
| 1094 | and then stops: below the platforms' 44px floor it stops being one. */ |
| 1095 | body.compact #corner { width: 50px; padding: 3px 3px 4px; } |
| 1096 | body.compact #talk.big { min-height: 38px; max-height: 44px; } |
| 1097 | body.compact #talk.big svg { width: 19px; height: 19px; } |
| 1098 | body.compact .tab-slot { max-width: calc(8rem + 2 * var(--tab-foot)); } |
| 1099 | body.compact button.tab { padding: 3px 6px 4px; } |
| 1100 | body.compact button.session-tab { padding: 2px 6px 3px; max-width: calc(8rem + 2 * var(--tab-foot)); } |
| 1101 | body.compact .tab-slot.closable button.tab { padding-right: 19px; } |
| 1102 | body.compact button.tab[aria-selected="true"] { padding-bottom: 9px; } |
| 1103 | body.compact button.tab-close { top: 4px; } |
| 1104 | body.compact .group-chip { padding: 1px 6px 2px; margin-left: 6px; font-size: 10px; } |
| 1105 | /* The count is what a folded chip has instead of tabs, so unlike a session |
| 1106 | tab's it survives compact. */ |
| 1107 | body.compact #term { padding: 2px; } |
| 1108 | |
| 1109 | /* Hidden: no header at all. A 4px strip along the top brings it back on |
| 1110 | hover, so the settings panel is never unreachable. */ |
| 1111 | body.chromeless header { |
| 1112 | position: absolute; top: 0; left: 0; right: 0; z-index: 5; |
| 1113 | opacity: 0; pointer-events: none; transition: opacity 90ms linear; |
| 1114 | } |
| 1115 | body.chromeless #reveal-strip { |
| 1116 | display: block; position: absolute; top: 0; left: 0; right: 0; |
| 1117 | height: 4px; z-index: 6; |
| 1118 | } |
| 1119 | body.chromeless #reveal-strip:hover ~ header, |
| 1120 | body.chromeless header:hover { opacity: 1; pointer-events: auto; } |
| 1121 | body.chromeless #term { padding: 2px; } |
| 1122 | #reveal-strip { display: none; } |
| 1123 | |
| 1124 | @keyframes pulse { 50% { opacity: .25; } } |
| 1125 | /* The spinner is driven from script, which parks it on a single frame when |
| 1126 | motion is reduced; this is only the resting glyphs' own breathing. They keep |
| 1127 | their colour, which is what carries the meaning — the pulse only ever made it |
| 1128 | easier to catch out of the corner of an eye. */ |
| 1129 | @media (prefers-reduced-motion: reduce) { |
| 1130 | button.tab .glyph.waiting, |
| 1131 | button.session-tab .glyph.waiting, |
| 1132 | .group-chip .glyph.waiting, |
| 1133 | button.tab .glyph.ready, |
| 1134 | button.session-tab .glyph.ready, |
| 1135 | .group-chip .glyph.ready { animation: none; } |
| 1136 | } |
| 1137 | /* `touch-action: none` because the terminal scrolls itself. Without it the |
| 1138 | browser claims a vertical drag as a pan of the panel, and having claimed it, |
| 1139 | stops sending the touchmove events sidebar.js turns into wheel events — the |
| 1140 | gesture would be swallowed before the terminal ever heard about it. A tap is |
| 1141 | untouched by this and still becomes a click. */ |
| 1142 | #term { |
| 1143 | flex: 1; min-height: 0; padding: 4px; overflow: hidden; |
| 1144 | touch-action: none; |
| 1145 | } |
| 1146 | #term .xterm { height: 100%; } |
| 1147 | /* --- settings, as a popup --------------------------------------------------- |
| 1148 | A floating card under the chevron, the way Chrome hangs its tab-search popup |
| 1149 | off the chevron in the same corner: rounded, lifted off the page by a shadow |
| 1150 | rather than joined to an edge, and scrolling inside itself so the terminal |
| 1151 | underneath never moves. `left` and `top` are set in JS from the chevron's own |
| 1152 | box, so it follows the button at any density. */ |
| 1153 | aside { |
| 1154 | position: fixed; z-index: 30; |
| 1155 | width: min(21rem, calc(100vw - 12px)); |
| 1156 | max-height: min(70vh, 34rem); overflow-y: auto; overscroll-behavior: contain; |
| 1157 | padding: 10px 12px 12px; |
| 1158 | background: var(--bg-panel); color: var(--fg); |
| 1159 | border: 1px solid var(--border); border-radius: 12px; |
| 1160 | box-shadow: 0 10px 30px var(--shadow), 0 2px 6px var(--shadow); |
| 1161 | } |
| 1162 | /* The card's own title bar, and a ✕ in it: a popup that floats has no edge to |
| 1163 | read as "this is a panel", so it says what it is and how to put it away. */ |
| 1164 | .settings-head { |
| 1165 | position: sticky; top: -10px; z-index: 1; |
| 1166 | display: flex; align-items: center; gap: 6px; |
| 1167 | margin: -10px -12px 8px; padding: 8px 8px 8px 12px; |
| 1168 | background: var(--bg-panel); |
| 1169 | border-bottom: 1px solid var(--border); |
| 1170 | border-radius: 12px 12px 0 0; |
| 1171 | } |
| 1172 | .settings-head button.icon { flex: none; width: 22px; height: 22px; } |
| 1173 | .settings-head button.icon svg { width: 12px; height: 12px; } |
| 1174 | .settings-head .title { |
| 1175 | flex: 1 1 auto; |
| 1176 | font-size: 11px; font-weight: 600; letter-spacing: .02em; |
| 1177 | } |
| 1178 | h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; |
| 1179 | color: var(--fg-muted); margin: 12px 0 4px; font-weight: 600; } |
| 1180 | h2:first-child { margin-top: 0; } |
| 1181 | /* The chevron points at what it opened, and turns over while it is open. */ |
| 1182 | #settings-toggle svg { transition: transform 120ms ease; } |
| 1183 | #settings-toggle[aria-expanded="true"] svg { transform: rotate(180deg); } |
| 1184 | #settings-toggle[aria-expanded="true"] { background: var(--border); color: var(--fg); } |
| 1185 | @media (prefers-reduced-motion: reduce) { |
| 1186 | #settings-toggle svg { transition: none; } |
| 1187 | } |
| 1188 | .hint { color: var(--fg-muted); margin: 0 0 6px; font-size: 11px; } |
| 1189 | .row { display: flex; gap: 6px; align-items: center; } |
| 1190 | input[type=text], input[type=password], input:not([type]), #url { |
| 1191 | width: 100%; background: var(--bg-input); color: var(--fg); |
| 1192 | border: 1px solid var(--border); border-radius: 4px; padding: 5px 7px; |
| 1193 | font-family: ui-monospace, monospace; font-size: 11px; |
| 1194 | } |
| 1195 | button { |
| 1196 | background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 4px; |
| 1197 | padding: 5px 10px; font-size: 11px; cursor: pointer; |
| 1198 | } |
| 1199 | button:hover { background: var(--accent-hover); } |
| 1200 | button.ghost { background: transparent; border: 1px solid var(--border); |
| 1201 | color: var(--fg-muted); padding: 3px 7px; } |
| 1202 | button.ghost:hover { background: var(--border); } |
| 1203 | pre { |
| 1204 | background: var(--bg-input); border: 1px solid var(--border); border-radius: 4px; |
| 1205 | padding: 6px; margin: 0 0 6px; overflow-x: auto; |
| 1206 | font-family: ui-monospace, monospace; font-size: 10px; |
| 1207 | white-space: pre-wrap; word-break: break-all; |
| 1208 | } |
| 1209 | #log { max-height: 120px; overflow-y: auto; color: var(--fg-muted); } |
| 1210 | code { font-family: ui-monospace, monospace; background: var(--bg-input); |
| 1211 | padding: 1px 3px; border-radius: 3px; font-size: 10px; } |
| 1212 | label.row { color: var(--fg-muted); font-size: 11px; margin-top: 5px; } |
| 1213 | label.row input { width: auto; } |
| 1214 | |
| 1215 | #trust-hint { margin-top: 8px; padding: 8px; border: 1px solid var(--warn-border); |
| 1216 | background: var(--warn-bg); border-radius: 4px; } |
| 1217 | .hint.warn { color: var(--warn); } |
| 1218 | |
| 1219 | /* Picked-element panel. A results card, not a toolbar: one primary action, |
| 1220 | the rest reduced to icons so the value itself is what draws the eye. */ |
| 1221 | #picked { |
| 1222 | flex: none; |
| 1223 | background: var(--bg-panel); |
| 1224 | border-bottom: 1px solid var(--border); |
| 1225 | border-top: 2px solid var(--accent); |
| 1226 | padding: 7px 8px 9px; |
| 1227 | } |
| 1228 | |
| 1229 | .picked-top { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } |
| 1230 | .chip { |
| 1231 | font-family: ui-monospace, monospace; font-size: 11px; |
| 1232 | color: var(--accent); background: var(--bg-input); |
| 1233 | border: 1px solid var(--border); border-radius: 4px; |
| 1234 | padding: 1px 6px; flex: none; |
| 1235 | } |
| 1236 | #picked-meta { |
| 1237 | flex: 1; min-width: 0; font-size: 11px; color: var(--fg-muted); |
| 1238 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| 1239 | } |
| 1240 | .picked-top button.icon { width: 22px; height: 22px; } |
| 1241 | .picked-top button.icon svg { width: 13px; height: 13px; } |
| 1242 | |
| 1243 | /* Segmented control instead of a native <select>: the options are few, short, |
| 1244 | and worth seeing all at once. */ |
| 1245 | .segmented { |
| 1246 | display: flex; gap: 2px; margin-bottom: 6px; |
| 1247 | background: var(--bg-input); |
| 1248 | border: 1px solid var(--border); border-radius: 6px; padding: 2px; |
| 1249 | } |
| 1250 | .segmented button { |
| 1251 | flex: 1 1 0; min-width: 0; |
| 1252 | background: transparent; color: var(--fg-muted); |
| 1253 | border: 0; border-radius: 4px; padding: 3px 4px; |
| 1254 | font-size: 10px; font-weight: 500; letter-spacing: .02em; |
| 1255 | cursor: pointer; white-space: nowrap; |
| 1256 | overflow: hidden; text-overflow: ellipsis; |
| 1257 | } |
| 1258 | .segmented button:hover { color: var(--fg); background: var(--border); } |
| 1259 | .segmented button[aria-selected="true"] { |
| 1260 | background: var(--accent); color: var(--accent-fg); |
| 1261 | } |
| 1262 | .segmented button[aria-selected="true"]:hover { background: var(--accent-hover); } |
| 1263 | |
| 1264 | #picked-value { |
| 1265 | background: var(--bg-input); |
| 1266 | border: 1px solid var(--border); border-radius: 6px; |
| 1267 | padding: 7px 8px; |
| 1268 | font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.45; |
| 1269 | color: var(--fg); |
| 1270 | word-break: break-all; white-space: pre-wrap; |
| 1271 | max-height: 84px; overflow-y: auto; |
| 1272 | user-select: all; |
| 1273 | } |
| 1274 | #picked-value.empty { color: var(--fg-muted); font-style: italic; user-select: none; } |
| 1275 | |
| 1276 | #picked-insert { width: 100%; margin-top: 7px; padding: 6px 10px; font-size: 11px; } |
| 1277 | #picked-warn { margin: 6px 0 0; font-size: 11px; } |
| 1278 | #picked-grant { width: 100%; margin-top: 6px; } |
| 1279 | |
| 1280 | select { font-family: inherit; } |
| 1281 | |
| 1282 | #density-hint { margin-top: 6px; } |
| 1283 | |
| 1284 | #font-row { margin-top: 7px; } |
| 1285 | #font-row .hint { margin: 0; margin-right: auto; } |
| 1286 | #font-row button.icon { width: 24px; height: 24px; border: 1px solid var(--border); } |
| 1287 | #font-size { font-variant-numeric: tabular-nums; font-size: 11px; min-width: 2ch; |
| 1288 | text-align: center; color: var(--fg); } |
| 1289 | #font-reset { padding: 4px 8px; font-size: 11px; } |
| 1290 | #theme-select, #density-select, #tabmode-select { width: auto; background: var(--bg-input); color: var(--fg); |
| 1291 | border: 1px solid var(--border); border-radius: 4px; padding: 5px 7px; font-size: 11px; } |