oxedyne/daimond/www/css/terminal.css
22.3 KiB, 1 run
created by r2519314175:1131, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | /* terminal.css — the terminal panel's own look, and the sixteen colours. |
| 2 | * |
| 3 | * Everything here is either a palette variable or derived from one, so the |
| 4 | * terminal wears whatever palette the app is wearing. The one thing that could |
| 5 | * not be derived is the ANSI set: a program asking for "red" is asking for a |
| 6 | * colour by name, and none of the app's tokens is a full sixteen-colour scheme. |
| 7 | * So there are two sets, chosen by `data-ink` — the axis the app already |
| 8 | * publishes for exactly this kind of question — and neither of them is |
| 9 | * per-palette, because sixteen colours times eleven palettes is eleven schemes |
| 10 | * to keep honest and one of them would rot. |
| 11 | * |
| 12 | * Measured against every palette's own `--bg-primary`, which is the ground the |
| 13 | * terminal is drawn on: every one of the sixteen clears 3.0 as LETTERING on |
| 14 | * every palette, worst case 3.19, and `dev/verify_terminal.mjs` holds them |
| 15 | * there. That is why "black" is a mid grey here — a program printing colour 0 |
| 16 | * on a dark ground is printing something a reader is entitled to be able to |
| 17 | * read, and an unreadable black is not more faithful, it is just unreadable. |
| 18 | * |
| 19 | * The awkward case is a name that has to mean two things. On paper, colour 7 as |
| 20 | * LETTERING must be dark or a prompt written in it is not there at all, while |
| 21 | * colour 7 as a BACKGROUND must be pale or a status bar comes out inverted. The |
| 22 | * usual light-terminal answer is to pick one and live with the other. Here the |
| 23 | * renderer knows which it is resolving, so `--term-ansi-bg-N` overrides the |
| 24 | * background value where the two genuinely differ, and both come out right. |
| 25 | * |
| 26 | * Amber deliberately holds blue out of everything it draws, and the ANSI set is |
| 27 | * NOT re-tinted for it. What a program prints is content, in the way a page in |
| 28 | * the Web panel is content: the app does not repaint either. A user reading at |
| 29 | * night has the palette for the frame around it. |
| 30 | */ |
| 31 | |
| 32 | :root { |
| 33 | /* Dark ground, light ink — the default. */ |
| 34 | --term-ansi-0: #767676; |
| 35 | --term-ansi-1: #EE7A6B; |
| 36 | --term-ansi-2: #8FCE7A; |
| 37 | --term-ansi-3: #DCAE58; |
| 38 | --term-ansi-4: #68AEE8; |
| 39 | --term-ansi-5: #CE93E2; |
| 40 | --term-ansi-6: #5FCBC8; |
| 41 | --term-ansi-7: #C9C4BB; |
| 42 | --term-ansi-8: #949494; |
| 43 | --term-ansi-9: #F7998C; |
| 44 | --term-ansi-10: #A9E294; |
| 45 | --term-ansi-11: #EDC97A; |
| 46 | --term-ansi-12: #8AC8F5; |
| 47 | --term-ansi-13: #E0AEF0; |
| 48 | --term-ansi-14: #84DFDD; |
| 49 | --term-ansi-15: #F3F0E9; |
| 50 | |
| 51 | /* A "black background" must actually be black. Colour 0 as LETTERING is |
| 52 | lifted to a grey so it can be read at all on a dark ground -- it clears |
| 53 | 3.19 on the palest of the dark palettes, which is the floor everything |
| 54 | else in the app is held to -- while as a GROUND it is the black the |
| 55 | program asked for. */ |
| 56 | --term-ansi-bg-0: #1A1A1A; |
| 57 | |
| 58 | --term-fg: var(--text-primary); |
| 59 | --term-bg: var(--bg-primary); |
| 60 | --term-cursor: var(--accent); |
| 61 | /* A translucent wash rather than a palette colour: it is painted OVER the |
| 62 | cell's own background, so it has to survive being laid on any of the |
| 63 | sixteen, and an opaque accent would hide the text it is selecting. |
| 64 | Written literally because nothing else in the app mixes colours, and one |
| 65 | `color-mix` here would be the only one. */ |
| 66 | --term-selection: rgba(255, 255, 255, .26); |
| 67 | --term-font: var(--font-mono); |
| 68 | /* One rung below body text, which is what dense monospace furniture uses |
| 69 | elsewhere in the app, and it moves with the user's text-size choice. */ |
| 70 | --term-font-size: var(--fs-sm); |
| 71 | } |
| 72 | |
| 73 | :root[data-ink="dark"] { |
| 74 | /* Light ground, dark ink. */ |
| 75 | --term-ansi-0: #242424; |
| 76 | --term-ansi-1: #A32A1E; |
| 77 | --term-ansi-2: #256D2A; |
| 78 | --term-ansi-3: #7A5A00; |
| 79 | --term-ansi-4: #1A5A96; |
| 80 | --term-ansi-5: #8A2E9C; |
| 81 | --term-ansi-6: #0F6E6C; |
| 82 | --term-ansi-7: #4E4E4E; |
| 83 | --term-ansi-8: #6E6E6E; |
| 84 | --term-ansi-9: #B93A29; |
| 85 | --term-ansi-10: #33843A; |
| 86 | --term-ansi-11: #8F6D08; |
| 87 | --term-ansi-12: #2668AC; |
| 88 | --term-ansi-13: #9C43AC; |
| 89 | --term-ansi-14: #177E7C; |
| 90 | --term-ansi-15: #1C1C1C; |
| 91 | /* The other side of the same problem. On paper, colours 7 and 15 have to be |
| 92 | dark to be readable AS TEXT — a prompt in bright white is otherwise not |
| 93 | there at all — and pale to be right AS A BACKGROUND, which is what a |
| 94 | status bar or a selected row uses them for. Both, then. */ |
| 95 | --term-ansi-bg-7: #D6D3CC; |
| 96 | --term-ansi-bg-15: #FFFFFF; |
| 97 | --term-ansi-bg-8: #9C9C9C; |
| 98 | |
| 99 | --term-selection: rgba(0, 0, 0, .20); |
| 100 | } |
| 101 | |
| 102 | /* ── The panel ──────────────────────────────────────────────── */ |
| 103 | |
| 104 | .term { |
| 105 | position: relative; |
| 106 | min-width: 0; |
| 107 | /* The grid is sized to the box, so the box may never be sized to the grid: |
| 108 | a canvas that pushed its container wider would grow itself on the next |
| 109 | measurement and never settle. */ |
| 110 | overflow: hidden; |
| 111 | background: var(--term-bg); |
| 112 | border: 1px solid var(--border-strong); |
| 113 | border-radius: var(--radius); |
| 114 | padding: 4px; |
| 115 | /* A terminal is a fixed grid, so it takes whatever room it is given and |
| 116 | computes columns from it rather than scrolling sideways. */ |
| 117 | width: 100%; |
| 118 | height: 100%; |
| 119 | box-sizing: border-box; |
| 120 | } |
| 121 | |
| 122 | /* Focus is drawn OUTSIDE the control, standing off from its edge against the |
| 123 | surface behind it, which is the rule the rest of the app follows: a ring made |
| 124 | out of the control's own border is only legible while the resting border is |
| 125 | nearly invisible, and this control has a strong resting border by design. */ |
| 126 | .term:focus-within { |
| 127 | outline: 2px solid var(--accent); |
| 128 | outline-offset: 2px; |
| 129 | } |
| 130 | |
| 131 | .term-canvas { |
| 132 | display: block; |
| 133 | max-width: 100%; |
| 134 | } |
| 135 | |
| 136 | /* The real control. It carries no text — anything typed or pasted into it is |
| 137 | sent and cleared — so it is placed over the canvas and made invisible rather |
| 138 | than hidden: a display:none field takes no keystrokes and gets no soft |
| 139 | keyboard on a phone. */ |
| 140 | .term-input { |
| 141 | position: absolute; |
| 142 | inset: 0; |
| 143 | width: 100%; |
| 144 | height: 100%; |
| 145 | margin: 0; |
| 146 | padding: 0; |
| 147 | border: 0; |
| 148 | resize: none; |
| 149 | overflow: hidden; |
| 150 | background: transparent; |
| 151 | color: transparent; |
| 152 | caret-color: transparent; |
| 153 | outline: none; |
| 154 | font: inherit; |
| 155 | /* Behind the paste question, which must be clickable. */ |
| 156 | z-index: 1; |
| 157 | /* AND IT MUST NOT TAKE THE MOUSE. Stretched over the canvas at `inset: 0`, this |
| 158 | field was the topmost thing under the pointer everywhere on the screen, so a |
| 159 | real drag hit the textarea and the canvas `mousedown` -- the whole of |
| 160 | selection -- never ran. Reported as "dragging doesn't select at all" on |
| 161 | 2026-08-26; selection had been built, painted and tested since the panel |
| 162 | shipped. `dev/verify_terminal.mjs` could not see it, because dispatching a |
| 163 | MouseEvent AT the canvas bypasses hit-testing: the check that catches this |
| 164 | asks `elementFromPoint` what is actually on top. |
| 165 | |
| 166 | The keyboard is unaffected -- focus is given by the canvas's own handlers, |
| 167 | which call `input.focus()` on mousedown and on touchstart, so a soft keyboard |
| 168 | still comes up on a phone. */ |
| 169 | pointer-events: none; |
| 170 | } |
| 171 | .term-input::selection { background: transparent; } |
| 172 | |
| 173 | /* The size, while it is changing. A person dragging a panel edge wants to know |
| 174 | what they are choosing; the rest of the time it is in the way. */ |
| 175 | .term-size { |
| 176 | position: absolute; |
| 177 | right: 10px; |
| 178 | bottom: 8px; |
| 179 | z-index: 2; |
| 180 | padding: 2px 7px; |
| 181 | border-radius: var(--radius-sm); |
| 182 | background: var(--bg-secondary); |
| 183 | border: 1px solid var(--border-2); |
| 184 | color: var(--text-secondary); |
| 185 | font: var(--fs-2xs) / 1.4 var(--font-mono); |
| 186 | opacity: 0; |
| 187 | transition: opacity .18s ease; |
| 188 | pointer-events: none; |
| 189 | } |
| 190 | .term-size.on { opacity: 1; } |
| 191 | |
| 192 | /* A bell is a flash of the panel's own edge, not a sound: a page that makes a |
| 193 | noise is a page people mute. */ |
| 194 | .term.bell { box-shadow: inset 0 0 0 2px var(--accent); } |
| 195 | |
| 196 | /* ── The paste question ─────────────────────────────────────── */ |
| 197 | |
| 198 | .term-paste { |
| 199 | position: absolute; |
| 200 | left: 8px; |
| 201 | right: 8px; |
| 202 | bottom: 8px; |
| 203 | z-index: 3; |
| 204 | padding: 10px 12px; |
| 205 | background: var(--bg-secondary); |
| 206 | border: 1px solid var(--border-strong); |
| 207 | border-radius: var(--radius); |
| 208 | box-shadow: 0 6px 18px rgba(0, 0, 0, .28); |
| 209 | } |
| 210 | .term-paste[hidden] { display: none; } |
| 211 | |
| 212 | .term-paste-say { |
| 213 | margin: 0 0 8px; |
| 214 | color: var(--text-primary); |
| 215 | font-size: var(--fs-sm); |
| 216 | line-height: 1.45; |
| 217 | } |
| 218 | |
| 219 | .term-paste-row { display: flex; flex-wrap: wrap; gap: 8px; } |
| 220 | |
| 221 | .term-paste-btn { |
| 222 | padding: 5px 12px; |
| 223 | border-radius: var(--radius-sm); |
| 224 | border: 1px solid var(--border-strong); |
| 225 | background: var(--bg-tertiary); |
| 226 | color: var(--text-primary); |
| 227 | font-size: var(--fs-sm); |
| 228 | cursor: pointer; |
| 229 | } |
| 230 | .term-paste-btn:hover { background: var(--bg-hover); } |
| 231 | .term-paste-btn.primary { |
| 232 | background: var(--accent); |
| 233 | border-color: var(--accent); |
| 234 | color: var(--on-fill); |
| 235 | } |
| 236 | .term-paste-btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } |
| 237 | .term-paste-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } |
| 238 | |
| 239 | /* ── What a screen reader is given ────────────────────────────── |
| 240 | Clipped rather than hidden. `display: none` and `visibility: hidden` both |
| 241 | take an element out of the accessibility tree, which would leave the canvas |
| 242 | as the only thing there — and a canvas says nothing at all. */ |
| 243 | |
| 244 | .term-sr { |
| 245 | position: absolute; |
| 246 | width: 1px; |
| 247 | height: 1px; |
| 248 | margin: -1px; |
| 249 | padding: 0; |
| 250 | overflow: hidden; |
| 251 | clip: rect(0 0 0 0); |
| 252 | clip-path: inset(50%); |
| 253 | white-space: nowrap; |
| 254 | border: 0; |
| 255 | } |
| 256 | |
| 257 | /* The transcript is many lines, so it may not be one nowrap line. */ |
| 258 | .term-mirror { white-space: normal; } |
| 259 | |
| 260 | /* ── The Terminal panel ───────────────────────────────────────── |
| 261 | The furniture AROUND the terminal: the head, the sentence saying where the |
| 262 | session is, the notices that ride beside the stream, and the line that says |
| 263 | how to get the keyboard back out. The terminal itself is everything above; |
| 264 | nothing here reaches inside it. */ |
| 265 | |
| 266 | /* The panel is a stage card and a flex column, like the AI, the Web page and a |
| 267 | document. app.css names the stage's cards one by one (`.ai, .web, .doc, .msg, |
| 268 | .compose, .tools`) and graph.css gives the Graph its own copy of that card for |
| 269 | exactly this reason; this is the Terminal's, declared here because this is the |
| 270 | file the terminal owns. |
| 271 | |
| 272 | Both halves of the rule earn their place. Left off the CARD list the panel |
| 273 | would be a bare box on the stage's ground with no edge between it and the |
| 274 | conversation beside it. Left off the FLEX list it would stay a block: its head |
| 275 | and its body would then share the WIDTH instead of the height, and nothing |
| 276 | inside it could be told to fill what is left. Spending was left off the dock's |
| 277 | equivalent once and came out as a stranded column of letters; the Terminal |
| 278 | comes out as a canvas frozen at its minimum grid however tall the panel is. |
| 279 | |
| 280 | No `flex` of its own: a stage occupant's flex and width are set inline by the |
| 281 | engine's applySeat, which is the only thing that knows whether it is sharing |
| 282 | the stage or holding it alone. */ |
| 283 | .panel.termpanel { |
| 284 | min-width: 0; |
| 285 | background: var(--bg-secondary); |
| 286 | border: 1px solid var(--border); |
| 287 | border-radius: var(--radius-lg); |
| 288 | display: flex; |
| 289 | flex-direction: column; |
| 290 | overflow: hidden; |
| 291 | } |
| 292 | |
| 293 | /* The head is the stage's card head, so the panel's own name is set in the same |
| 294 | type as the AI's and the page's rather than in the dock's small capitals. The |
| 295 | name is fixed and the program's title takes the slack beside it, so the three |
| 296 | buttons stay against the right however long a title runs. */ |
| 297 | .termpanel .chead { gap: 8px; } |
| 298 | .termpanel .chead .ctitle { flex: none; } |
| 299 | |
| 300 | .termpanel .termp-card { |
| 301 | display: flex; |
| 302 | flex-direction: column; |
| 303 | gap: 6px; |
| 304 | min-height: 0; |
| 305 | /* The card takes the panel's height so the host below can be told to fill |
| 306 | what is left -- a terminal computes its columns from the box it is given, |
| 307 | so the box has to be a real one. */ |
| 308 | flex: 1 1 auto; |
| 309 | padding: 8px; |
| 310 | box-sizing: border-box; |
| 311 | } |
| 312 | |
| 313 | /* The host is the only thing that grows. `min-height: 0` because a flex item |
| 314 | defaults to its content's size, and a canvas that had already grown would |
| 315 | then keep the box at that size for ever. */ |
| 316 | .termpanel .termp-host { |
| 317 | flex: 1 1 auto; |
| 318 | min-height: 120px; |
| 319 | display: flex; |
| 320 | /* The containing block for the notices, which lie over the SCREEN and not |
| 321 | over the state line above it. */ |
| 322 | position: relative; |
| 323 | } |
| 324 | .termpanel .termp-host > .term { flex: 1 1 auto; } |
| 325 | |
| 326 | /* A terminal that was refused before it ever drew anything is an empty black |
| 327 | box under a paragraph explaining why it is empty. The paragraph is the whole |
| 328 | of what there is to read, so it gets the panel. The moment a session starts — |
| 329 | or once anything has been drawn — the box comes back. */ |
| 330 | .termpanel.no-term .termp-host { display: none; } |
| 331 | |
| 332 | /* What the program calls itself, beside the panel's own name. It takes the head's |
| 333 | slack, so the buttons keep their place. Truncated rather than wrapped: a title |
| 334 | is a label, and a two-line head moves the terminal. */ |
| 335 | .termp-title { |
| 336 | flex: 1 1 auto; |
| 337 | min-width: 0; |
| 338 | overflow: hidden; |
| 339 | text-overflow: ellipsis; |
| 340 | white-space: nowrap; |
| 341 | color: var(--text-secondary); |
| 342 | font: var(--fs-xs) / 1.4 var(--font-mono); |
| 343 | } |
| 344 | |
| 345 | /* The three buttons sit closer together than the stage head's default 8px: they |
| 346 | are icons in a row and read as one control group, not as three. */ |
| 347 | .termpanel .chead-right { gap: 2px; } |
| 348 | |
| 349 | /* Where the session is, in a sentence. Refusals arrive here verbatim, so it has |
| 350 | to be able to hold a paragraph without pushing the terminal off the panel. */ |
| 351 | .termp-state { |
| 352 | margin: 0; |
| 353 | color: var(--text-secondary); |
| 354 | font-size: var(--fs-xs); |
| 355 | line-height: 1.5; |
| 356 | /* Two lines, whatever it says, and it scrolls rather than grows. A state line |
| 357 | that changed height would take that height out of the terminal below it, |
| 358 | the grid would be recomputed smaller, and the model would push the top of |
| 359 | the screen into scrollback — so "the program exited" or "output is missing" |
| 360 | would sweep away the output it is about. Fixed, nothing moves. */ |
| 361 | flex: none; |
| 362 | height: 3em; |
| 363 | overflow-y: auto; |
| 364 | } |
| 365 | .termp-state:empty { display: none; } |
| 366 | /* With no screen to protect, the sentence is the whole panel and takes the room: |
| 367 | this is the refusal case, where there is nothing else to read. */ |
| 368 | .termpanel.no-term .termp-state { height: auto; max-height: 14em; } |
| 369 | .termpanel.no-term .termp-state.refused { |
| 370 | padding: 8px 10px 8px 10px; |
| 371 | border: 1px solid var(--border-strong); |
| 372 | border-left: 3px solid var(--warn); |
| 373 | border-radius: var(--radius-sm); |
| 374 | background: var(--bg-secondary); |
| 375 | } |
| 376 | /* A refusal is not a status line. It is the one thing on the panel worth |
| 377 | reading, so it is marked rather than merely coloured -- a coloured sentence is |
| 378 | invisible to a reader who cannot see the colour. |
| 379 | |
| 380 | A rule down its left and nothing else, because the mark must not change the |
| 381 | BOX: padding and a border would make the state line taller the moment a |
| 382 | session was refused or a hole appeared, and the terminal underneath would lose |
| 383 | the top of its screen to the reflow. The full frame belongs to the case where |
| 384 | there is no screen to protect, below. */ |
| 385 | .termp-state.refused { |
| 386 | border-left: 3px solid var(--warn); |
| 387 | padding-left: 8px; |
| 388 | color: var(--text-primary); |
| 389 | } |
| 390 | |
| 391 | /* A hole in the output, said beside the stream. Each notice stays until the |
| 392 | session ends: a gap is a fact about the transcript above it, and a message |
| 393 | that faded would leave a screen nobody can account for. |
| 394 | |
| 395 | OVER the terminal rather than above it, and that is not a decorative choice. |
| 396 | A notice in the flow takes its height out of the panel, the grid is recomputed |
| 397 | smaller, and the model pushes the lines at the top of the screen into |
| 398 | scrollback — so the arrival of "output is missing here" would itself sweep the |
| 399 | output around the hole out of view, at the one moment a reader needs to see |
| 400 | it. Out of flow, the grid does not move and nothing is swept anywhere. */ |
| 401 | .termp-gaps { |
| 402 | position: absolute; |
| 403 | left: 6px; |
| 404 | right: 6px; |
| 405 | top: 6px; |
| 406 | z-index: 4; |
| 407 | display: flex; |
| 408 | flex-direction: column; |
| 409 | gap: 4px; |
| 410 | max-height: 45%; |
| 411 | overflow-y: auto; |
| 412 | } |
| 413 | .termp-gaps:empty { display: none; } |
| 414 | .termp-gap { |
| 415 | display: flex; |
| 416 | align-items: flex-start; |
| 417 | gap: 6px; |
| 418 | padding: 6px 8px; |
| 419 | border: 1px solid var(--border-strong); |
| 420 | border-left: 3px solid var(--warn); |
| 421 | border-radius: var(--radius-sm); |
| 422 | background: var(--bg-secondary); |
| 423 | color: var(--text-primary); |
| 424 | font-size: var(--fs-xs); |
| 425 | line-height: 1.45; |
| 426 | } |
| 427 | |
| 428 | /* Take one notice off the screen it is lying over. The fact it reports does not |
| 429 | go with it — the count stays in the state line — so this is a "move it out of |
| 430 | my way" and not a "forget it". A real button with a real name: the glyph on it |
| 431 | is a decoration and says nothing to a screen reader. */ |
| 432 | .termp-gap-x { |
| 433 | flex: none; |
| 434 | margin: -4px -4px 0 auto; |
| 435 | /* A hit area a finger can find, around a glyph that is four pixels wide. */ |
| 436 | min-width: 26px; |
| 437 | min-height: 26px; |
| 438 | display: flex; |
| 439 | align-items: center; |
| 440 | justify-content: center; |
| 441 | border: 1px solid transparent; |
| 442 | border-radius: var(--radius-sm); |
| 443 | background: transparent; |
| 444 | color: var(--text-secondary); |
| 445 | font-size: var(--fs-xs); |
| 446 | line-height: 1.4; |
| 447 | cursor: pointer; |
| 448 | } |
| 449 | .termp-gap-x:hover { color: var(--text-primary); border-color: var(--border-strong); } |
| 450 | .termp-gap-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } |
| 451 | |
| 452 | /* The way out, in writing. Quiet, and always there: a hint that appeared only |
| 453 | while the terminal had focus would be unreadable at the moment it is wanted, |
| 454 | because the reader is looking at the cursor. */ |
| 455 | .termp-foot { |
| 456 | margin: 0; |
| 457 | color: var(--text-secondary); |
| 458 | font-size: var(--fs-2xs); |
| 459 | line-height: 1.4; |
| 460 | } |
| 461 | /* With no terminal on the panel there is nothing to move the keyboard out of, |
| 462 | and a hint about a control that is not there is one more thing to read past. */ |
| 463 | .termpanel.no-term .termp-foot { display: none; } |
| 464 | |
| 465 | /* The head's own bell mark, for a bell that rings while the eye is elsewhere. |
| 466 | The terminal flashes its own edge; this is the same event at panel scale, and |
| 467 | it is aria-hidden because a bell is not something to announce twice. */ |
| 468 | .termp-bell { |
| 469 | width: 8px; |
| 470 | height: 8px; |
| 471 | border-radius: 50%; |
| 472 | background: var(--accent); |
| 473 | opacity: 0; |
| 474 | transition: opacity .2s ease; |
| 475 | } |
| 476 | .termpanel.bell .termp-bell { opacity: 1; } |
| 477 | |
| 478 | /* The heading is a focus target: F6 out of the terminal lands here, and a |
| 479 | programmatic focus with no ring is a keyboard user who has lost their place. */ |
| 480 | .termpanel .chead [role="heading"]:focus-visible { |
| 481 | outline: 2px solid var(--accent); |
| 482 | outline-offset: 2px; |
| 483 | border-radius: var(--radius-sm); |
| 484 | } |
| 485 | |
| 486 | @media (prefers-reduced-motion: reduce) { |
| 487 | .termp-bell { transition: none; } |
| 488 | } |
| 489 | |
| 490 | /* ── Narrow screens ─────────────────────────────────────────── */ |
| 491 | |
| 492 | @media (max-width: 560px) { |
| 493 | .term { |
| 494 | padding: 2px; |
| 495 | /* Below about twenty columns a terminal stops being a terminal, so the |
| 496 | floor is held and the panel scrolls rather than the type shrinking |
| 497 | until nothing can be read. The scroll is INSIDE the panel: the page |
| 498 | itself never scrolls sideways. */ |
| 499 | overflow-x: auto; |
| 500 | } |
| 501 | .term-size { right: 6px; bottom: 6px; } |
| 502 | } |
| 503 | |
| 504 | /* A reader who has asked for less motion gets a steady cursor; the blink is |
| 505 | turned off in terminal.js, and this stops the bell flashing too. */ |
| 506 | @media (prefers-reduced-motion: reduce) { |
| 507 | .term-size { transition: none; } |
| 508 | .term.bell { box-shadow: inset 0 0 0 2px var(--border-strong); } |
| 509 | } |
| 510 | |
| 511 | /* ── What a right-click offers ──────────────────────────────── */ |
| 512 | |
| 513 | /* A canvas's own context menu is the browser's, and it offers to save the |
| 514 | terminal as an image. This replaces it with the three things a person |
| 515 | right-clicks a terminal for. */ |
| 516 | |
| 517 | .term-menu { |
| 518 | position: absolute; |
| 519 | z-index: 4; |
| 520 | min-width: 148px; |
| 521 | padding: 4px; |
| 522 | background: var(--bg-secondary); |
| 523 | border: 1px solid var(--border-strong); |
| 524 | border-radius: var(--radius); |
| 525 | box-shadow: 0 6px 18px rgba(0, 0, 0, .28); |
| 526 | display: flex; |
| 527 | flex-direction: column; |
| 528 | } |
| 529 | .term-menu[hidden] { display: none; } |
| 530 | |
| 531 | .term-menu-item { |
| 532 | padding: 6px 10px; |
| 533 | border: 0; |
| 534 | border-radius: var(--radius-sm); |
| 535 | background: transparent; |
| 536 | color: var(--text-primary); |
| 537 | font-size: var(--fs-sm); |
| 538 | text-align: left; |
| 539 | cursor: pointer; |
| 540 | } |
| 541 | .term-menu-item:hover:not(:disabled) { background: var(--bg-hover); } |
| 542 | .term-menu-item:disabled { color: var(--text-secondary); cursor: default; } |
| 543 | |
| 544 | /* ── The folder rows in Settings ─────────────────────────────── */ |
| 545 | |
| 546 | /* The app's own control box -- the one `.settings-select` wears -- rather than |
| 547 | `.admin-item`, which is a full-width borderless list row and read as a |
| 548 | misplaced menu entry beside a pulldown. */ |
| 549 | |
| 550 | .termroot-btn { |
| 551 | background: var(--bg-tertiary); |
| 552 | border: 1px solid var(--border-strong); |
| 553 | border-radius: var(--radius-sm); |
| 554 | color: var(--text-primary); |
| 555 | padding: 9px 12px; |
| 556 | font-size: var(--fs-base); |
| 557 | cursor: pointer; |
| 558 | text-align: left; |
| 559 | } |
| 560 | .termroot-btn:hover { background: var(--bg-hover); } |
| 561 | .termroot-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); } |
| 562 | |
| 563 | .termroot-acts { display: flex; flex-wrap: wrap; gap: 8px; } |
| 564 | |
| 565 | /* The folder a terminal will open in, stated rather than offered in a list: a |
| 566 | pulldown of somebody's particular directories is a guess, not a choice. */ |
| 567 | .termroot-now { |
| 568 | display: block; |
| 569 | margin: 0 0 8px; |
| 570 | font-family: var(--font-mono); |
| 571 | font-size: var(--fs-2xs); |
| 572 | color: var(--text-primary); |
| 573 | word-break: break-all; |
| 574 | } |
| 575 | .termroot-now[data-default="1"] { color: var(--text-secondary); } |
| 576 | |
| 577 | /* ── The folder walk ─────────────────────────────────────────── */ |
| 578 | |
| 579 | /* Real folders with real paths, listed by the hand. The browser's own picker |
| 580 | cannot serve this: it answers with a handle carrying a name and no path, and |
| 581 | a fence needs the path. */ |
| 582 | |
| 583 | .termroot-walk { |
| 584 | display: flex; |
| 585 | flex-direction: column; |
| 586 | gap: 2px; |
| 587 | max-height: 320px; |
| 588 | overflow-y: auto; |
| 589 | margin-top: 8px; |
| 590 | padding: 6px; |
| 591 | border: 1px solid var(--border-strong); |
| 592 | border-radius: var(--radius-sm); |
| 593 | background: var(--bg-tertiary); |
| 594 | } |
| 595 | .termroot-walk .admin-note { |
| 596 | position: sticky; |
| 597 | top: -6px; |
| 598 | padding: 4px 6px; |
| 599 | background: var(--bg-tertiary); |
| 600 | color: var(--text-secondary); |
| 601 | font-family: var(--font-mono); |
| 602 | font-size: var(--fs-2xs); |
| 603 | word-break: break-all; |
| 604 | } |
| 605 | /* The rows ARE a list, so they keep the list idiom; only the buttons beside a |
| 606 | field wear the field's box. */ |
| 607 | .termroot-walk .admin-item { font-family: var(--font-mono); font-size: var(--fs-xs); } |
| 608 | .termroot-walk .termroot-btn { margin: 4px 0 6px; } |