Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 18m ago 4b8c9e1d
history blame raw 96.6 KiB
1/* Vole's web surface. Token-driven: a light theme is another block of variables,
2 and a real identity is a change to the top block only. Everforest is the
3 permanent theme in static/everforest.css; this sheet supplies the layout and
4 the fallback tokens the theme sheet overrides.
5
6 Sections, in file order. Rules cascade in this order too, so a later section
7 can override an earlier one and a section cannot be moved without checking
8 what it was quietly overriding:
9
10 tokens custom properties, dark and light (:root)
11 chrome document shell, global bar, account menu,
12 command palette
13 repository header name, visibility, tab row
14 panels and tables the bordered surfaces lists and pages sit on
15 controls buttons, inputs, selects, form layout
16 labels, flashes, empty pages
17 auth sign-in, registration, invite, account
18 repository home clone button, readme, the home row layout
19 diffs and markdown code/diff tables, hunks, comment cells, .md
20 shared responsive rules the few rules that apply to every surface
21 repository overview summary page: main column and sidebar
22 code tab bar and crumbs the bar above the open file
23 code browser and file tree the persistent tree beside the pane
24 syntax highlighting prism token classes
25 repository overview sidebar compact commit and run rows
26 run detail summary cards, step timing and lazy logs
27 issues and changes list pages, thread, forms, metadata sidebar
28 icons inline SVG sizing
29 activity feed event rows for one repository and the instance
30 accessibility skip link, focus rings, reduced motion
31 light theme the second token block
32 pager previous/next between list pages
33 code search per-file result blocks
34 blame per-line commit column
35 landing page introduction card, repository grid, feed rail
36 status page instance storage, runners, configuration
37 language colours file-icon and language-dot palette
38
39 Note the two pairs that are easy to confuse: "repository home" is the clone
40 button and readme on /<name>, while "landing page" is the instance home at
41 /; "repository overview" is the summary page, while "repository overview
42 sidebar" is the small commit and run rows it uses. */
43
44:root {
45 color-scheme: dark;
46
47 /* surfaces */
48 --bg: #0e0f11;
49 --bg-panel: #141517;
50 --bg-raised: #191a1e;
51 --bg-hover: #1f2024;
52 --bg-active: #24262b;
53 --border: #26282d;
54 --border-strong: #35383f;
55
56 /* text */
57 --fg: #e7e8ea;
58 --fg-muted: #8b909a;
59 --fg-faint: #696e78;
60
61 /* accent and status */
62 --accent: #7c86ff;
63 --accent-fg: #ffffff;
64 --accent-soft: rgba(124, 134, 255, 0.14);
65 /* A run that is still going gets its own token rather than --accent: a theme
66 whose accent is green (Everforest) would otherwise paint "running" and
67 "success" the same colour, and the two must not be confused at a glance. */
68 --running: var(--accent);
69 --good: #4cb782;
70 --good-soft: rgba(76, 183, 130, 0.12);
71 --bad: #eb5757;
72 --bad-soft: rgba(235, 87, 87, 0.12);
73 --warn: #f2c94c;
74 --warn-soft: rgba(242, 201, 76, 0.14);
75 /* Status buttons are filled, so their label uses the page ground colour. It is
76 dark in the dark themes and light in the light ones, which keeps it legible
77 against every theme's good/bad/warn hue. */
78 --status-fg: var(--bg);
79 /* A merged request has landed, which is neither open nor merely closed. It
80 gets its own hue so a merged row cannot be read as still being reviewed. */
81 --merged: #b083f0;
82 --merged-soft: rgba(176, 131, 240, 0.14);
83
84 /* shape and type */
85 --radius: 6px;
86 --radius-lg: 8px;
87 /* One width for every search input: the global bar and a page's filter row
88 should not resize the field when the reader moves between them. */
89 --search-w: 18rem;
90 /* One measure for the page column and the global bar. The bar pads itself out
91 to the page's content edges, so the brand and the account menu sit on the
92 same vertical line as the content below them. */
93 --page-max: 1440px;
94 --page-gutter: 24px;
95 --font: "Lexend", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", "Liberation Sans", Arial, sans-serif;
96 --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
97 --fs: 15.4px;
98 --lh: 1.55;
99 /* Every explicit size is calc(<px> * var(--font-scale)) so the Account page's
100 Text size preset reaches labels and code, not just body copy. */
101 --font-scale: 1;
102}
103
104/* Text-size presets. The account page stores the choice in this browser and
105 static/theme.js applies the attribute before the first paint. */
106:root[data-font-scale="small"] { --font-scale: 0.9; }
107:root[data-font-scale="medium"] { --font-scale: 1; }
108:root[data-font-scale="large"] { --font-scale: 1.1; }
109
110/* Self-hosted, OFL fonts. The Latin/Latin-ext subsets keep the binary small;
111 anything outside them falls through to the system stack in --font/--mono. */
112@font-face {
113 font-family: "Lexend";
114 font-style: normal;
115 font-weight: 100 900;
116 font-display: swap;
117 src: url("/-/static/fonts/lexend-latin-ext.woff2") format("woff2");
118 unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
119}
120@font-face {
121 font-family: "Lexend";
122 font-style: normal;
123 font-weight: 100 900;
124 font-display: swap;
125 src: url("/-/static/fonts/lexend-latin.woff2") format("woff2");
126 unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
127}
128@font-face {
129 font-family: "JetBrains Mono";
130 font-style: normal;
131 font-weight: 100 800;
132 font-display: swap;
133 src: url("/-/static/fonts/jetbrains-mono-latin-ext.woff2") format("woff2");
134 unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
135}
136@font-face {
137 font-family: "JetBrains Mono";
138 font-style: normal;
139 font-weight: 100 800;
140 font-display: swap;
141 src: url("/-/static/fonts/jetbrains-mono-latin.woff2") format("woff2");
142 unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
143}
144@font-face {
145 font-family: "JetBrains Mono";
146 font-style: italic;
147 font-weight: 100 800;
148 font-display: swap;
149 src: url("/-/static/fonts/jetbrains-mono-italic-latin-ext.woff2") format("woff2");
150 unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
151}
152@font-face {
153 font-family: "JetBrains Mono";
154 font-style: italic;
155 font-weight: 100 800;
156 font-display: swap;
157 src: url("/-/static/fonts/jetbrains-mono-italic-latin.woff2") format("woff2");
158 unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
159}
160
161* { box-sizing: border-box; }
162* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
163::-webkit-scrollbar { width: 10px; height: 10px; }
164::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; }
165::-webkit-scrollbar-track { background: transparent; }
166
167html, body { height: 100%; }
168
169body {
170 margin: 0;
171 background: var(--bg);
172 color: var(--fg);
173 font-family: var(--font);
174 font-size: calc(var(--fs) * var(--font-scale));
175 line-height: var(--lh);
176 -webkit-font-smoothing: auto;
177 font-kerning: normal;
178}
179
180a { color: var(--accent); text-decoration: none; }
181a:hover { text-decoration: underline; }
182
183h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.005em; }
184h1 { font-size: calc(19.8px * var(--font-scale)); line-height: 1.35; margin: 2px 0 12px; }
185h2 { font-size: calc(14.3px * var(--font-scale)); color: var(--fg-muted); margin: 24px 0 8px; }
186h3 { font-size: calc(14.3px * var(--font-scale)); }
187p { margin: 0 0 10px; }
188ul, ol { margin: 0 0 10px; padding-left: 20px; }
189hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
190img { max-width: 100%; }
191
192.muted { color: var(--fg-muted); }
193.sha { font-family: var(--mono); font-size: calc(13.2px * var(--font-scale)); color: var(--fg-muted); }
194.nowrap { white-space: nowrap; }
195.adds { color: var(--good); }
196.dels { color: var(--bad); }
197.conflict, .error { color: var(--bad); }
198.bad { color: var(--bad); }
199
200/* ---------- chrome ---------- */
201
202.topbar {
203 display: flex;
204 align-items: center;
205 gap: 20px;
206 height: 46px;
207 /* The bar spans the window; its items stay inside the page column. */
208 padding-block: 0;
209 padding-inline: max(var(--page-gutter), calc((100% - var(--page-max)) / 2 + var(--page-gutter)));
210 background: color-mix(in srgb, var(--bg-panel) 92%, transparent);
211 border-bottom: 1px solid var(--border);
212 position: relative;
213 z-index: 20;
214 backdrop-filter: blur(8px);
215}
216
217/* The brand is the emblem alone. The link keeps an accessible name, so a mark with
218 no text beside it is still "Vole home" to a screen reader. */
219.brand {
220 display: inline-flex;
221 align-items: center;
222 color: var(--fg);
223}
224.brand:hover { text-decoration: none; }
225
226/* The mark is a mask in the theme's ink. A light theme draws it in the page
227 colour with a dark outline instead, because the same muted ink on a pale bar
228 reads as a smudge. A custom light theme can opt in by setting these tokens. */
229:root {
230 --mark-fill: currentColor;
231 --mark-outline: transparent;
232}
233:root[data-theme^="everforest-light"],
234:root[data-theme="catppuccin-latte"],
235:root[data-theme="solarized-light"],
236:root[data-theme="rose-pine-dawn"],
237:root[data-theme="ayu-light"] {
238 --mark-fill: var(--bg);
239 --mark-outline: var(--fg);
240}
241.brand .mark {
242 display: block;
243 position: relative;
244 flex: none;
245 width: 30px;
246 height: 30px;
247}
248.brand .mark::before,
249.brand .mark::after {
250 content: "";
251 position: absolute;
252 inset: 0;
253 -webkit-mask-position: center;
254 -webkit-mask-size: contain;
255 -webkit-mask-repeat: no-repeat;
256 mask-position: center;
257 mask-size: contain;
258 mask-repeat: no-repeat;
259}
260/* The outline layer is a slightly expanded solid silhouette behind the light
261 head. It draws the outside edge and shows through the eye, mouth, and
262 inner-ear holes; the whisker gaps still show the bar. */
263.brand .mark::before {
264 background-color: var(--mark-outline);
265 -webkit-mask-image: url("/-/static/vole-head-solid.png");
266 mask-image: url("/-/static/vole-head-solid.png");
267}
268.brand .mark::after {
269 background-color: var(--mark-fill);
270 -webkit-mask-image: url("/-/static/vole-head.png");
271 mask-image: url("/-/static/vole-head.png");
272}
273
274.topnav { display: flex; gap: 6px; }
275.topnav a {
276 display: inline-flex;
277 align-items: center;
278 gap: 6px;
279 padding: 5px 11px;
280 border: 1px solid transparent;
281 border-radius: var(--radius);
282 color: var(--fg);
283 font-size: calc(13.75px * var(--font-scale));
284 font-weight: 500;
285 white-space: nowrap;
286}
287.topnav a:hover {
288 background: var(--bg-hover);
289 border-color: var(--border);
290 color: var(--fg);
291 text-decoration: none;
292}
293.topnav a.active {
294 background: var(--accent-soft);
295 border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
296 color: var(--accent);
297}
298
299.topsearch {
300 flex: 1 1 auto;
301 min-width: 8rem;
302 max-width: var(--search-w);
303 margin-left: auto;
304}
305/* The reader is opening the palette, not editing a field, so this is a button
306 shaped like the search input it replaces. The label stands in for the
307 placeholder, and the last item names the shortcut the browser will hear. */
308.topsearch-box {
309 display: flex;
310 align-items: center;
311 gap: 8px;
312 width: 100%;
313 min-width: 0;
314 min-height: 30px;
315 padding: 5px 8px;
316 border: 1px solid var(--border-strong);
317 border-radius: var(--radius);
318 background: var(--bg);
319 color: var(--fg-muted);
320 font: inherit;
321 font-size: calc(13.75px * var(--font-scale));
322 text-align: left;
323 cursor: pointer;
324}
325.topsearch-box:hover {
326 border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
327 color: var(--fg);
328}
329.topsearch-box .icon { width: 13px; height: 13px; color: var(--fg-faint); }
330.topsearch-label {
331 flex: 1 1 auto;
332 min-width: 0;
333 overflow: hidden;
334 text-overflow: ellipsis;
335 white-space: nowrap;
336}
337.topsearch-key {
338 flex: 0 0 auto;
339 padding: 0 4px;
340 border: 1px solid var(--border);
341 border-radius: 4px;
342 background: var(--bg-panel);
343 color: var(--fg-faint);
344 font-size: calc(11.55px * var(--font-scale));
345}
346
347.topright {
348 display: flex;
349 align-items: center;
350 gap: 12px;
351 font-size: calc(13.75px * var(--font-scale));
352 color: var(--fg-muted);
353}
354.topright > a { display: inline-flex; align-items: center; gap: 5px; color: var(--fg-muted); }
355.topright > a:hover { color: var(--fg); text-decoration: none; }
356
357.account-menu { position: relative; }
358.account-menu > summary {
359 display: flex;
360 align-items: center;
361 gap: 6px;
362 padding: 4px 8px;
363 border-radius: var(--radius);
364 color: var(--fg-muted);
365 white-space: nowrap;
366 list-style: none;
367 cursor: pointer;
368}
369.account-menu > summary::-webkit-details-marker { display: none; }
370.account-menu > summary:hover { background: var(--bg-hover); color: var(--fg); }
371.account-menu[open] > summary { background: var(--bg-raised); color: var(--fg); }
372.account-name { max-width: 12rem; overflow: hidden; text-overflow: ellipsis; }
373.account-caret { display: inline-flex; color: var(--fg-faint); }
374.account-caret .icon { width: 12px; height: 12px; transform: rotate(90deg); transition: transform 0.12s ease; }
375.account-menu[open] .account-caret .icon { transform: rotate(-90deg); }
376.account-pop {
377 position: absolute;
378 top: calc(100% + 6px);
379 right: 0;
380 z-index: 30;
381 width: 13rem;
382 padding: 4px;
383 background: var(--bg-raised);
384 border: 1px solid var(--border-strong);
385 border-radius: var(--radius-lg);
386 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
387}
388.account-pop a,
389.account-pop button {
390 display: flex;
391 align-items: center;
392 justify-content: flex-start;
393 gap: 8px;
394 width: 100%;
395 padding: 6px 8px;
396 border: 0;
397 border-radius: var(--radius);
398 background: none;
399 color: var(--fg);
400 font: inherit;
401 font-size: calc(13.75px * var(--font-scale));
402 text-align: left;
403 cursor: pointer;
404}
405.account-pop a:hover,
406.account-pop button:hover { background: var(--bg-hover); color: var(--accent); text-decoration: none; }
407.account-pop form { margin: 0; }
408.account-sep { border-top: 1px solid var(--border); margin: 4px 0; }
409
410/* ---------- command palette ---------- */
411
412/* The global bar's search opens this: one field over every issue, change and
413 comment the reader can see. A row is a link, so Enter and a click take the
414 same route; the stylesheet only has to make the selected row visible. */
415.palette {
416 width: min(40rem, calc(100vw - 2 * var(--page-gutter)));
417 max-height: min(80vh, 44rem);
418 margin: 10vh auto auto;
419 padding: 0;
420 border: 1px solid var(--border-strong);
421 border-radius: var(--radius-lg);
422 background: var(--bg-raised);
423 color: var(--fg);
424 box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
425 overflow: hidden;
426}
427.palette::backdrop { background: rgba(0, 0, 0, 0.5); }
428.palette-search {
429 display: flex;
430 align-items: center;
431 gap: 10px;
432 padding: 10px 12px;
433 border-bottom: 1px solid var(--border);
434}
435.palette-search .icon { width: 16px; height: 16px; color: var(--fg-muted); }
436.palette-search input[type="search"] {
437 flex: 1 1 auto;
438 width: auto;
439 max-width: none;
440 min-height: 0;
441 padding: 2px 0;
442 border: 0;
443 background: transparent;
444 font-size: calc(15.4px * var(--font-scale));
445}
446/* The field is the dialog's heading, not a control with a box of its own. */
447.palette-search input[type="search"]:focus-visible { outline: 0; }
448.palette-close {
449 display: inline-flex;
450 align-items: center;
451 justify-content: center;
452 padding: 4px;
453 border: 0;
454 border-radius: var(--radius);
455 background: none;
456 color: var(--fg-faint);
457 cursor: pointer;
458}
459.palette-close:hover { background: var(--bg-hover); color: var(--fg); }
460.palette-results { max-height: min(60vh, 30rem); overflow-y: auto; padding: 6px; }
461.palette-row {
462 display: block;
463 padding: 7px 9px;
464 border-radius: var(--radius);
465 color: var(--fg);
466}
467.palette-row:hover { background: var(--bg-hover); text-decoration: none; }
468.palette-row.is-selected { background: var(--accent-soft); }
469.palette-row-title {
470 display: block;
471 font-weight: 500;
472 overflow: hidden;
473 text-overflow: ellipsis;
474 white-space: nowrap;
475}
476.palette-row-where {
477 display: block;
478 color: var(--fg-faint);
479 font-size: calc(12.65px * var(--font-scale));
480}
481/* A palette row is one line high per part; the editable page can afford the
482 wrapped excerpt, the dialog cannot. */
483.palette-row .search-snippet {
484 margin: 0;
485 overflow: hidden;
486 text-overflow: ellipsis;
487 white-space: nowrap;
488}
489.palette-empty {
490 margin: 0;
491 padding: 16px 12px;
492 color: var(--fg-muted);
493 font-size: calc(13.75px * var(--font-scale));
494}
495.palette-foot {
496 display: flex;
497 gap: 14px;
498 padding: 7px 12px;
499 border-top: 1px solid var(--border);
500 color: var(--fg-faint);
501 font-size: calc(12.65px * var(--font-scale));
502}
503.palette-foot kbd {
504 padding: 0 4px;
505 border: 1px solid var(--border);
506 border-radius: 4px;
507 background: var(--bg-panel);
508 color: var(--fg-muted);
509 font-size: calc(11.55px * var(--font-scale));
510}
511
512.page {
513 width: min(100%, var(--page-max));
514 margin: 0 auto;
515 padding: 20px var(--page-gutter) 72px;
516}
517
518@media (max-width: 720px) {
519 .tabs { mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent); }
520 :root { --page-gutter: 12px; }
521 .topbar {
522 height: auto;
523 flex-wrap: wrap;
524 padding-block: 8px;
525 gap: 8px 12px;
526 }
527 .topsearch { order: 3; flex: 1 1 100%; max-width: none; margin-left: 0; }
528 .topsearch-box { width: 100%; }
529 /* A keyboard shortcut is not part of a phone's vocabulary. */
530 .topsearch-key { display: none; }
531 .topright { margin-left: auto; }
532 .topnav { gap: 4px; }
533 .topnav a { padding: 4px 8px; }
534 /* The icon is enough on a phone; the menu keeps the full name. */
535 .account-name { display: none; }
536 .page { padding: 16px var(--page-gutter) 56px; }
537 .palette { margin-top: 6vh; }
538}
539
540/* ---------- repository header ---------- */
541
542.repohead {
543 margin-bottom: 16px;
544 border-bottom: 1px solid var(--border);
545}
546.repo-title { padding: 26px 0 12px; }
547/* The repository actions sit on the title line, at the far right of the header. */
548.repo-title-row {
549 display: flex;
550 align-items: center;
551 gap: 16px;
552}
553.repo-title-row .repo-name { flex: 1 1 auto; min-width: 0; }
554.repo-title-actions {
555 display: flex;
556 align-items: center;
557 gap: 8px;
558 flex: 0 0 auto;
559}
560.repo-title-row .clone-pop { left: auto; right: 0; }
561.repo-name {
562 display: flex;
563 align-items: center;
564 gap: 8px;
565 min-width: 0;
566 color: var(--fg);
567 font-size: calc(28.6px * var(--font-scale));
568 line-height: 1.2;
569 font-weight: 600;
570 letter-spacing: -0.025em;
571}
572.repo-name .repo-vis .icon { width: 16px; height: 16px; }
573.repo-desc {
574 margin: 6px 0 0;
575 color: var(--fg-muted);
576 font-size: calc(14.3px * var(--font-scale));
577 overflow: hidden;
578 text-overflow: ellipsis;
579 white-space: nowrap;
580}
581
582/* The tab pages are a level below the repository identity: their own headers
583 stay small. Top-level pages (Runners, Activity, Accounts, Account, Status) keep
584 the home-page header scale. */
585.repohead + .page-head {
586 align-items: flex-start;
587 gap: 16px;
588 padding: 0;
589 margin-bottom: 12px;
590 border-bottom: 0;
591}
592.repohead + .page-head h1 {
593 font-size: calc(19.8px * var(--font-scale));
594 line-height: 1.35;
595 letter-spacing: -0.005em;
596 margin: 0 0 3px;
597}
598.repohead + .page-head h1:last-child { margin-bottom: 0; }
599.repohead + .page-head .page-sub { font-size: calc(13.75px * var(--font-scale)); }
600
601@media (max-width: 720px) {
602 .repo-title { padding: 18px 0 10px; }
603 .repo-name { font-size: calc(24.2px * var(--font-scale)); }
604 .repo-name .repo-vis .icon { width: 14px; height: 14px; }
605}
606
607.tabs {
608 display: flex;
609 gap: 2px;
610 overflow-x: auto;
611 scrollbar-width: none;
612 margin-left: -10px;
613}
614.tabs::-webkit-scrollbar { display: none; }
615.tabs a {
616 color: var(--fg-muted);
617 padding: 8px 10px 9px;
618 border-bottom: 2px solid transparent;
619 white-space: nowrap;
620 font-size: calc(13.75px * var(--font-scale));
621}
622.tabs a:hover { color: var(--fg); text-decoration: none; }
623.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); }
624.tabs .chip-count { margin-left: 0; }
625.tabs a.active .chip-count { color: var(--fg-muted); }
626
627/* The rule under a tab row belongs to a wrapper, not to the strip that carries
628 the labels: `.tabs` pulls itself 10px left so the first label's text sits on
629 the content edge, and a border on that element would hang 10px past it. The
630 repository header keeps its rule on `.repohead`; a page's tabs and a change's
631 sections wrap the same strip in this class, so every row starts and ends on
632 the content edges together. */
633.tabrow { margin-bottom: 14px; border-bottom: 1px solid var(--border); }
634
635/* A count that moved while the reader was looking elsewhere keeps one short
636 ring, so the change is noticed; the number itself is the answer. app.js adds
637 the class when a poll changes a badge and drops it when the animation ends. */
638.tabs .chip-count.count-changed {
639 border-radius: 999px;
640 animation: count-changed 1.8s ease-out;
641}
642@keyframes count-changed {
643 from { color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
644 to { box-shadow: 0 0 0 3px transparent; }
645}
646
647/* ---------- panels and tables ---------- */
648
649.panel {
650 border: 1px solid var(--border);
651 border-radius: var(--radius-lg);
652 overflow: hidden;
653 margin: 12px 0;
654 background: var(--bg-panel);
655}
656
657.card {
658 background: var(--bg-panel);
659 border: 1px solid var(--border);
660 border-radius: var(--radius-lg);
661 padding: 14px;
662 margin: 12px 0;
663}
664
665table {
666 width: 100%;
667 border-collapse: collapse;
668 font-size: calc(13.75px * var(--font-scale));
669}
670.panel > table { margin: 0; }
671th, td {
672 text-align: left;
673 padding: 8px 12px;
674 border-bottom: 1px solid var(--border);
675 vertical-align: top;
676}
677tr:last-child > td { border-bottom: 0; }
678th {
679 color: var(--fg-faint);
680 font-weight: 500;
681 font-size: calc(12.1px * var(--font-scale));
682 text-transform: uppercase;
683 letter-spacing: 0.04em;
684 background: var(--bg-raised);
685 padding-top: 7px;
686 padding-bottom: 7px;
687}
688tbody tr:hover { background: var(--bg-hover); }
689
690table.commits td { padding-top: 7px; padding-bottom: 7px; }
691table.commits td:first-child { width: 9ch; }
692
693/* ---------- controls ---------- */
694
695button, .button {
696 display: inline-flex;
697 align-items: center;
698 justify-content: center;
699 gap: 6px;
700 background: var(--accent);
701 color: var(--accent-fg);
702 border: 1px solid transparent;
703 border-radius: var(--radius);
704 padding: 5px 11px;
705 font-family: var(--font);
706 font-size: calc(13.75px * var(--font-scale));
707 font-weight: 500;
708 line-height: 1.4;
709 cursor: pointer;
710 white-space: nowrap;
711}
712button:hover, .button:hover { filter: brightness(1.08); text-decoration: none; }
713button:disabled { opacity: 0.5; cursor: default; }
714
715/* A POST form that is still in flight keeps its button in place: it is disabled
716 and, after a short delay added by app.js, its icon becomes a spinner. */
717button[data-loading] { cursor: progress; }
718button.spinning > .icon { display: none; }
719.spinner {
720 width: 14px;
721 height: 14px;
722 flex: 0 0 auto;
723 border: 2px solid currentColor;
724 border-right-color: transparent;
725 border-radius: 50%;
726 animation: spin 0.7s linear infinite;
727}
728@keyframes spin { to { transform: rotate(1turn); } }
729
730.button.secondary, button.secondary {
731 background: var(--bg-raised);
732 color: var(--fg);
733 border-color: var(--border-strong);
734}
735.button.secondary:hover { background: var(--bg-hover); }
736
737/* Action colours use the same filled shape as the primary control: the
738 background carries the meaning, and hover brightens it like every other
739 button. */
740.button.success, button.success {
741 background: var(--good);
742 color: var(--status-fg);
743 border-color: transparent;
744}
745.button.warn, button.warn {
746 background: var(--warn);
747 color: var(--status-fg);
748 border-color: transparent;
749}
750.button.danger, button.danger {
751 background: var(--bad);
752 color: var(--status-fg);
753 border-color: transparent;
754}
755
756button.link, .link {
757 background: none;
758 border: 0;
759 color: var(--accent);
760 padding: 0;
761 font: inherit;
762 font-weight: 400;
763}
764button.link:hover, .link:hover { text-decoration: underline; }
765form.inline { display: inline; }
766
767select, input[type="text"], input[type="password"], input[type="search"], textarea {
768 background: var(--bg);
769 color: var(--fg);
770 border: 1px solid var(--border-strong);
771 border-radius: var(--radius);
772 padding: 5px 8px;
773 font: inherit;
774 font-size: calc(13.75px * var(--font-scale));
775 min-height: 30px;
776}
777select:focus, input:focus, textarea:focus {
778 outline: 2px solid var(--accent-soft);
779 outline-offset: 1px;
780 border-color: var(--accent);
781}
782input[type="text"], input[type="password"], input[type="search"], textarea {
783 width: 100%;
784 max-width: 34rem;
785}
786textarea { min-height: 7rem; resize: vertical; line-height: 1.5; }
787label { display: block; margin: 10px 0; color: var(--fg-muted); font-size: calc(13.75px * var(--font-scale)); }
788/* The label text sits above the control. Without this, a max-width input shares
789 the line with its label and the form reads as a row of pairs. */
790label > input, label > textarea, label > select {
791 display: block;
792 margin-top: 4px;
793 color: var(--fg);
794}
795label > select { width: 100%; max-width: 34rem; }
796label > .field-hint {
797 display: block;
798 margin-top: 4px;
799 color: var(--fg-faint);
800 font-size: calc(12.1px * var(--font-scale));
801}
802
803/* Every filter/search row is one component: controls in a wrapping row, a
804 visible label on every select, selects that apply on change, and a Search
805 button for the text field. Search inputs share --search-w on every surface. */
806.filters {
807 display: flex;
808 flex-wrap: wrap;
809 align-items: center;
810 gap: 8px;
811 margin: 10px 0 12px;
812}
813/* The code page needs a form per ref picker (two refs in one form would submit
814 twice), so the row can contain forms as well as be one. */
815.filters form {
816 display: inline-flex;
817 flex-wrap: wrap;
818 align-items: center;
819 gap: 8px;
820}
821/* Every row reads left to right: filters, then search. The search group takes
822 the leftover space, so it sits against the right edge whether the row is one
823 page's chips and two selects or another page's ref pickers. A row with no
824 search puts its clear action in the same slot. */
825.filters-search {
826 display: flex;
827 align-items: center;
828 gap: 8px;
829 margin-left: auto;
830}
831/* Inside the row the chips are one group, not a row with its own margins. */
832.filters .chips { margin: 0; }
833.filters input[type="text"],
834.filters input[type="search"] { width: var(--search-w); max-width: 100%; }
835/* A label and its control stay on one line when the filter row wraps. */
836.filter-field {
837 display: inline-flex;
838 align-items: center;
839 gap: 6px;
840 margin: 0;
841}
842.filter-field > input,
843.filter-field > select {
844 width: auto;
845 max-width: none;
846 margin-top: 0;
847}
848.chips + .filters { margin-top: 0; }
849.page-head + .filters { margin-top: 0; }
850
851@media (max-width: 720px) {
852 .filters { flex-direction: column; align-items: stretch; }
853 .filters form { flex-direction: column; align-items: stretch; }
854 .filters input[type="text"],
855 .filters input[type="search"] { width: 100%; }
856 .filters-search { margin-left: 0; width: 100%; }
857 /* The search stays one line on a phone: a full-width field and its button,
858 not the stacked label/control shape the ref pickers need. */
859 .filters-search form { flex-direction: row; align-items: center; }
860 .filters-search input[type="text"],
861 .filters-search input[type="search"] { flex: 1 1 auto; width: auto; }
862 /* A phone stacks the label above its control: a label and an 18rem select
863 cannot share a 20rem-wide row without the control overflowing it. */
864 .filter-field { flex-direction: column; align-items: stretch; gap: 4px; }
865 .filters .filter-field > select { width: 100%; }
866}
867
868/* ---------- labels, flashes, empty pages ---------- */
869
870.label {
871 display: inline-block;
872 margin-left: 6px;
873 padding: 1px 6px;
874 border: 1px solid var(--border-strong);
875 border-radius: 10px;
876 font-size: calc(12.1px * var(--font-scale));
877 line-height: 1.5;
878 color: var(--fg-muted);
879 vertical-align: 1px;
880}
881a.label:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
882
883.flash {
884 border: 1px solid var(--border-strong);
885 background: var(--bg-panel);
886 border-radius: var(--radius);
887 padding: 8px 12px;
888 margin-bottom: 14px;
889 font-size: calc(13.75px * var(--font-scale));
890}
891.flash.bad { border-color: color-mix(in srgb, var(--bad) 50%, var(--border)); background: var(--bad-soft); }
892.flash.good { border-color: color-mix(in srgb, var(--good) 50%, var(--border)); background: var(--good-soft); }
893.flash.warn { border-color: color-mix(in srgb, var(--warn) 50%, var(--border)); background: var(--warn-soft); }
894
895.empty-page {
896 max-width: 34rem;
897 margin: 12vh auto 0;
898 text-align: center;
899}
900.empty-page .eyebrow {
901 color: var(--fg-faint);
902 font-family: var(--mono);
903 font-size: calc(12.1px * var(--font-scale));
904 font-weight: 600;
905 line-height: 1;
906 letter-spacing: 0.1em;
907 text-transform: uppercase;
908 margin-bottom: 10px;
909}
910.empty-page h1 { font-size: calc(26.4px * var(--font-scale)); margin-bottom: 8px; }
911.empty-page p { margin-bottom: 18px; }
912
913/* ---------- auth ---------- */
914
915.auth { max-width: 34rem; margin-top: 8vh; }
916.auth h1 { margin-bottom: 6px; }
917
918/* ---------- repository home ---------- */
919
920details.clone { position: relative; }
921details.clone > summary {
922 list-style: none;
923 cursor: pointer;
924 display: inline-flex;
925 align-items: center;
926 gap: 6px;
927 background: var(--bg-raised);
928 color: var(--fg);
929 border: 1px solid var(--border-strong);
930 border-radius: var(--radius);
931 padding: 5px 11px;
932 font-size: calc(13.75px * var(--font-scale));
933}
934details.clone > summary::-webkit-details-marker { display: none; }
935details.clone > summary:hover { background: var(--bg-hover); }
936details.clone[open] > summary { border-color: var(--accent); }
937.clone-pop {
938 position: absolute;
939 z-index: 10;
940 top: calc(100% + 6px);
941 left: 0;
942 width: min(32rem, 80vw);
943 background: var(--bg-raised);
944 border: 1px solid var(--border-strong);
945 border-radius: var(--radius-lg);
946 padding: 12px;
947 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
948}
949.clone-pop p { margin: 8px 0 2px; }
950.clone-pop code { display: block; }
951/* Each advertised transport is one row: the command takes the width, the copy
952 button stays put at its end. */
953.clone-field {
954 display: flex;
955 align-items: center;
956 gap: 6px;
957}
958.clone-field code {
959 flex: 1 1 auto;
960 min-width: 0;
961 overflow-x: auto;
962 white-space: nowrap;
963}
964.clone-field button { flex: 0 0 auto; padding: 5px 8px; }
965
966.row { display: flex; gap: 16px; flex-wrap: wrap; }
967.row > div { flex: 1 1 18rem; min-width: 0; }
968
969.readme { margin: 16px 0; }
970
971/* ---------- diffs and markdown ---------- */
972
973code, pre, kbd {
974 font-family: var(--mono);
975}
976code {
977 background: var(--bg-raised);
978 border: 1px solid var(--border);
979 border-radius: 4px;
980 padding: 1px 5px;
981 font-size: calc(12.65px * var(--font-scale));
982}
983pre {
984 background: var(--bg-panel);
985 border: 1px solid var(--border);
986 border-radius: var(--radius);
987 padding: 12px 14px;
988 overflow: auto;
989 font-size: calc(13.2px * var(--font-scale));
990 line-height: 1.55;
991 margin: 10px 0;
992}
993pre code { background: none; border: 0; padding: 0; font-size: inherit; }
994
995.crumbs { color: var(--fg-muted); font-size: calc(13.75px * var(--font-scale)); margin: -4px 0 12px; }
996.crumbs a { color: var(--fg-muted); }
997.crumbs a:hover { color: var(--accent); }
998.dir { color: var(--accent); }
999
1000.md { line-height: 1.65; max-width: 90ch; }
1001.md > :first-child { margin-top: 0; }
1002.md > :last-child { margin-bottom: 0; }
1003.md h1, .md h2, .md h3, .md h4 {
1004 color: var(--fg);
1005 margin: 20px 0 8px;
1006 letter-spacing: -0.01em;
1007 text-transform: none;
1008}
1009.md h1 { font-size: calc(22px * var(--font-scale)); }
1010.md h2 { font-size: calc(17.6px * var(--font-scale)); }
1011.md h3 { font-size: calc(15.4px * var(--font-scale)); }
1012.md h4 { font-size: calc(14.3px * var(--font-scale)); }
1013.md p { margin: 9px 0; }
1014.md ul, .md ol { padding-left: 22px; margin: 9px 0; }
1015.md blockquote {
1016 margin: 10px 0;
1017 padding: 2px 0 2px 12px;
1018 border-left: 3px solid var(--border-strong);
1019 color: var(--fg-muted);
1020}
1021.md pre { background: var(--bg-panel); }
1022.md table { width: auto; }
1023.md img { border-radius: var(--radius); }
1024
1025.diffsum { font-size: calc(13.2px * var(--font-scale)); margin: 10px 0; }
1026
1027/* A diff is read with the browser's find bar as much as with the eyes, so the
1028 card is not a scrolling box: it grows to the full height of the patch and the
1029 page scrolls instead. `clip` rounds the corners like `hidden` did, without
1030 making the element a scroll container that find has to scroll separately. */
1031.diff {
1032 border: 1px solid var(--border);
1033 border-radius: var(--radius-lg);
1034 margin: 12px 0;
1035 overflow: clip;
1036 background: var(--bg-panel);
1037}
1038.diff h3 {
1039 display: flex;
1040 align-items: center;
1041 gap: 8px;
1042 margin: 0;
1043 padding: 8px 12px;
1044 background: var(--bg-raised);
1045 border-bottom: 1px solid var(--border);
1046 font-size: calc(13.2px * var(--font-scale));
1047 font-weight: 550;
1048}
1049/* The name gives way before the counts do: the path can be long enough to cut,
1050 and two short numbers at the end of the row are the reason the row exists. */
1051.diff h3 .path {
1052 flex: 1 1 auto;
1053 min-width: 0;
1054 overflow: hidden;
1055 text-overflow: ellipsis;
1056 white-space: nowrap;
1057 font-family: var(--mono);
1058}
1059/* One file's counts, in the same row as its name and at the other end of it. */
1060.diffstat {
1061 display: inline-flex;
1062 align-items: center;
1063 gap: 8px;
1064 flex: 0 0 auto;
1065 font-family: var(--mono);
1066 font-size: calc(12.1px * var(--font-scale));
1067 font-weight: 400;
1068}
1069/* The row's own gap spaces the badges; the label's standalone margin was for
1070 prose, where nothing else spaced it. */
1071.diff h3 .label { margin-left: 0; }
1072.diff table {
1073 font-family: var(--mono);
1074 font-size: calc(12.65px * var(--font-scale));
1075 border-collapse: collapse;
1076 table-layout: fixed;
1077}
1078.diff col.comment-col { width: 2.25rem; }
1079.diff col.ln-col { width: 3.5rem; }
1080.diff td {
1081 border: 0;
1082 padding: 1px 10px;
1083 vertical-align: top;
1084 line-height: 19px;
1085}
1086.diff tbody tr:hover { background: none; }
1087.diff td.ln {
1088 text-align: right;
1089 color: var(--fg-faint);
1090 user-select: none;
1091 border-right: 1px solid var(--border);
1092 padding: 1px 8px;
1093 white-space: nowrap;
1094}
1095/* Only the code column keeps spaces and wraps. The gutter cells hold the
1096 template's own newlines and indentation; with pre-wrap those became blank
1097 line boxes and every diff row looked padded. */
1098.diff td.code { padding-left: 12px; white-space: pre-wrap; word-break: break-word; }
1099.diff tr.add { background: var(--good-soft); }
1100.diff tr.add td.ln { color: var(--good); }
1101.diff tr.del { background: var(--bad-soft); }
1102.diff tr.del td.ln { color: var(--bad); }
1103.diff tr.hunk td {
1104 color: var(--fg-muted);
1105 background: var(--bg-raised);
1106 padding: 5px 12px;
1107 font-size: calc(12.1px * var(--font-scale));
1108 border-top: 1px solid var(--border);
1109 border-bottom: 1px solid var(--border);
1110}
1111.diff tr.note td {
1112 background: var(--bg-panel);
1113 padding: 8px 12px;
1114 font-size: calc(13.2px * var(--font-scale));
1115}
1116.inline-note {
1117 padding: 8px 10px;
1118 border: 1px solid var(--border);
1119 border-radius: var(--radius);
1120 background: var(--bg-raised);
1121 scroll-margin-top: 16px;
1122}
1123.inline-note + .inline-note { margin-top: 8px; }
1124.inline-note:target { border-color: var(--accent); }
1125.inline-note:target .inline-note-head .comment-author { color: var(--accent); }
1126.inline-note-head {
1127 display: flex;
1128 align-items: center;
1129 gap: 6px;
1130 margin-bottom: 5px;
1131 color: var(--fg-muted);
1132 font-size: calc(12.65px * var(--font-scale));
1133}
1134.inline-note-head .comment-author { color: var(--fg); font-size: calc(13.2px * var(--font-scale)); }
1135.inline-note .comment-body {
1136 padding: 0;
1137 max-width: none;
1138 font-size: calc(13.75px * var(--font-scale));
1139 line-height: 1.55;
1140}
1141.inline-note .comment-body p { margin: 4px 0; }
1142.diff tr.inline-comment td {
1143 background: var(--bg-panel);
1144 padding: 8px 12px;
1145 border-top: 1px solid var(--border);
1146}
1147.inline-comment-form {
1148 display: flex;
1149 flex-direction: column;
1150 gap: 8px;
1151 max-width: 52rem;
1152}
1153.inline-comment-card {
1154 display: flex;
1155 flex-direction: column;
1156 gap: 8px;
1157 padding: 8px 10px;
1158 border: 1px solid var(--border);
1159 border-radius: var(--radius);
1160 background: var(--bg-raised);
1161}
1162.inline-comment-head {
1163 display: flex;
1164 align-items: center;
1165 justify-content: space-between;
1166 gap: 12px;
1167 color: var(--fg-muted);
1168 font-size: calc(13.2px * var(--font-scale));
1169}
1170.inline-comment-head code { color: var(--fg); }
1171.inline-comment-form textarea { min-height: 5.5rem; max-width: none; }
1172.inline-comment-form .form-actions { margin-top: 0; }
1173.diff .binary { padding: 10px 12px; color: var(--fg-muted); font-size: calc(13.2px * var(--font-scale)); margin: 0; }
1174
1175/* ---------- shared responsive rules ---------- */
1176
1177@media (max-width: 720px) {
1178 h1 { font-size: calc(17.6px * var(--font-scale)); }
1179 .repohead { gap: 0 10px; }
1180 th, td { padding: 7px 9px; }
1181 .nowrap { white-space: normal; }
1182}
1183
1184/* ---------- repository overview ---------- */
1185
1186.overview {
1187 display: grid;
1188 grid-template-columns: minmax(0, 1fr) 340px;
1189 gap: 28px;
1190 align-items: start;
1191}
1192.overview-main { min-width: 0; }
1193.overview-side section { padding: 12px 0 14px; border-bottom: 1px solid var(--border); }
1194.overview-side section:first-child { padding-top: 0; }
1195.overview-side section:last-child { border-bottom: 0; }
1196.overview-side h2 { margin: 0 0 8px; }
1197
1198.with-more { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
1199.with-more .more { font-size: calc(13.2px * var(--font-scale)); font-weight: 400; color: var(--fg-muted); }
1200.with-more .more:hover { color: var(--accent); }
1201
1202.side-list { display: flex; flex-direction: column; gap: 6px; }
1203.side-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: calc(13.75px * var(--font-scale)); }
1204.side-row .muted { font-size: calc(12.65px * var(--font-scale)); white-space: nowrap; }
1205
1206.contributors { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
1207.contributors li { display: flex; align-items: center; flex-wrap: wrap; gap: 2px 8px; min-width: 0; }
1208.contributors .cname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: calc(13.75px * var(--font-scale)); }
1209.contributors .muted { font-size: calc(12.65px * var(--font-scale)); }
1210.contributors .contributor-stats { display: inline-flex; align-items: center; gap: 8px; color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); }
1211.contributors .contrib-stat { position: relative; display: inline-flex; align-items: center; gap: 3px; cursor: help; }
1212.contributors .contrib-stat .icon { width: 13px; height: 13px; color: var(--fg-faint); }
1213.contributors .contrib-stat:hover,
1214.contributors .contrib-stat:focus-visible { color: var(--fg); outline: none; }
1215.contributors .contrib-stat:hover .icon,
1216.contributors .contrib-stat:focus-visible .icon { color: var(--accent); }
1217
1218.lang { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; margin-bottom: 8px; font-size: calc(13.2px * var(--font-scale)); }
1219.lang .muted { font-size: calc(12.65px * var(--font-scale)); }
1220.bar {
1221 grid-column: 1 / -1;
1222 appearance: none;
1223 width: 100%;
1224 height: 5px;
1225 border: 0;
1226 border-radius: 3px;
1227 background: var(--bg-active);
1228 overflow: hidden;
1229}
1230.bar::-webkit-progress-bar { background: var(--bg-active); border-radius: 3px; }
1231.bar::-webkit-progress-value { background: var(--accent); border-radius: 3px; }
1232.bar::-moz-progress-bar { background: var(--accent); border-radius: 3px; }
1233
1234.filehead {
1235 display: flex;
1236 align-items: center;
1237 justify-content: space-between;
1238 gap: 12px;
1239 padding: 7px 12px;
1240 background: var(--bg-raised);
1241 border: 1px solid var(--border);
1242 border-bottom: 0;
1243 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1244 font-family: var(--mono);
1245 font-size: calc(13.2px * var(--font-scale));
1246}
1247.filehead .icon { width: 13px; height: 13px; margin-right: 6px; vertical-align: -2px; }
1248.filehead a { font-family: var(--font); font-size: calc(12.65px * var(--font-scale)); color: var(--fg-muted); }
1249.filehead a:hover { color: var(--accent); }
1250.readme {
1251 background: var(--bg-panel);
1252 border: 1px solid var(--border);
1253 border-radius: 0 0 var(--radius-lg) var(--radius-lg);
1254 padding: 18px 20px;
1255 margin: 0 0 20px;
1256 max-width: none;
1257}
1258.readme > :first-child { margin-top: 0; }
1259
1260.latest {
1261 display: flex;
1262 flex-wrap: wrap;
1263 align-items: baseline;
1264 gap: 4px 10px;
1265 padding: 7px 12px;
1266 margin: 0 0 16px;
1267 background: var(--bg-panel);
1268 border: 1px solid var(--border);
1269 border-radius: var(--radius);
1270 font-size: calc(13.75px * var(--font-scale));
1271}
1272.latest .muted { font-size: calc(12.65px * var(--font-scale)); }
1273.latest a.sha { margin-left: auto; }
1274
1275.status {
1276 display: inline-flex;
1277 align-items: center;
1278 gap: 5px;
1279 font-size: calc(12.1px * var(--font-scale));
1280 font-weight: 500;
1281 line-height: 1.4;
1282 text-transform: uppercase;
1283 letter-spacing: 0.04em;
1284 white-space: nowrap;
1285 color: var(--fg-muted);
1286}
1287.status::before {
1288 content: "";
1289 width: 6px;
1290 height: 6px;
1291 border-radius: 50%;
1292 background: currentColor;
1293}
1294.status.success { color: var(--good); }
1295.status.failed { color: var(--bad); }
1296.status.running { color: var(--running); }
1297.status.queued { color: var(--warn); }
1298.status.error { color: var(--warn); }
1299.status.canceled { color: var(--fg-faint); }
1300
1301.chip {
1302 display: inline-flex;
1303 align-items: center;
1304 gap: 5px;
1305 padding: 3px 9px;
1306 border: 1px solid var(--border-strong);
1307 border-radius: 12px;
1308 font-size: calc(12.65px * var(--font-scale));
1309 color: var(--fg-muted);
1310 background: var(--bg-panel);
1311}
1312.chip .icon { width: 12px; height: 12px; }
1313.chip:hover { color: var(--fg); border-color: var(--fg-faint); text-decoration: none; }
1314.chip.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
1315
1316td.fit { width: 1%; white-space: nowrap; }
1317
1318@media (max-width: 1040px) {
1319 .overview { grid-template-columns: 1fr; }
1320 .overview-side { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 24px; }
1321 .overview-side section:first-child { padding-top: 12px; }
1322}
1323
1324/* ---------- code tab bar and crumbs ---------- */
1325
1326/* The tab bar and the tree header share a height so their content starts on the
1327 same line. The bar is the file's identity, not a separate title block. */
1328.code-tabbar {
1329 display: flex;
1330 align-items: center;
1331 gap: 8px;
1332 height: 36px;
1333 min-width: 0;
1334 padding: 0 10px;
1335 background: var(--bg-raised);
1336 border: 1px solid var(--border);
1337 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1338 color: var(--fg-muted);
1339 font-family: var(--mono);
1340 font-size: calc(12.65px * var(--font-scale));
1341}
1342.code-tab-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); }
1343.code-tab-icon .icon { width: 13px; height: 13px; }
1344.code-crumbs {
1345 display: flex;
1346 align-items: center;
1347 gap: 6px;
1348 min-width: 0;
1349 overflow: hidden;
1350 white-space: nowrap;
1351}
1352.code-crumbs a { color: var(--fg-muted); }
1353.code-crumbs a:hover { color: var(--accent); }
1354.code-crumbs .sep { color: var(--fg-faint); }
1355.code-crumbs .current { color: var(--fg); overflow: hidden; text-overflow: ellipsis; }
1356.code-tab-actions {
1357 display: flex;
1358 flex: 0 0 auto;
1359 align-items: center;
1360 gap: 10px;
1361 margin-left: auto;
1362 font-family: var(--font);
1363 font-size: calc(12.65px * var(--font-scale));
1364}
1365.code-tab-actions a { color: var(--fg-muted); }
1366.code-tab-actions a:hover,
1367.code-tab-actions a.active { color: var(--accent); }
1368.code-tab-actions .muted { font-size: calc(12.65px * var(--font-scale)); }
1369.code-tabbar + .panel,
1370.code-tabbar + .filebody,
1371.code-tabbar + .card,
1372.code-tabbar + .readme {
1373 margin-top: 0;
1374 border-top: 0;
1375 border-top-left-radius: 0;
1376 border-top-right-radius: 0;
1377}
1378
1379/* ---------- code browser and file tree ---------- */
1380
1381.code-browser {
1382 display: grid;
1383 grid-template-columns: 260px minmax(0, 1fr);
1384 gap: 18px;
1385 align-items: start;
1386}
1387.code-main { min-width: 0; scroll-margin-top: 16px; }
1388
1389.file-tree {
1390 display: flex;
1391 flex-direction: column;
1392 position: sticky;
1393 top: 16px;
1394 max-height: calc(100vh - 32px);
1395 overflow: hidden;
1396 background: var(--bg-panel);
1397 border: 1px solid var(--border);
1398 border-radius: var(--radius-lg);
1399}
1400.tree-head {
1401 display: flex;
1402 align-items: center;
1403 justify-content: space-between;
1404 gap: 8px;
1405 height: 36px;
1406 padding: 0 10px;
1407 background: var(--bg-raised);
1408 border-bottom: 1px solid var(--border);
1409 color: var(--fg-muted);
1410 font-size: calc(12.1px * var(--font-scale));
1411 letter-spacing: 0.05em;
1412 text-transform: uppercase;
1413}
1414.tree-head .icon { width: 13px; height: 13px; margin-right: 5px; vertical-align: -2px; }
1415.tree-head .muted { font-size: calc(11.55px * var(--font-scale)); letter-spacing: 0; text-transform: none; }
1416.tree-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 6px 4px 8px; }
1417
1418.tree-list { list-style: none; margin: 0; padding: 0; }
1419.tree-children { margin-left: 9px; padding-left: 5px; border-left: 1px solid var(--border); }
1420.tree-row {
1421 display: flex;
1422 align-items: center;
1423 gap: 6px;
1424 min-height: 24px;
1425 padding: 2px 7px;
1426 border-radius: 4px;
1427 color: var(--fg-muted);
1428 font-size: calc(13.2px * var(--font-scale));
1429 line-height: 1.45;
1430}
1431summary.tree-row { list-style: none; cursor: pointer; }
1432summary.tree-row::-webkit-details-marker { display: none; }
1433.tree-row:hover { background: var(--bg-hover); color: var(--fg); text-decoration: none; }
1434.tree-row.active,
1435.tree-dir.active > summary.tree-row { background: var(--accent-soft); color: var(--accent); }
1436.tree-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); }
1437.tree-row.active .tree-icon { color: var(--accent); }
1438.tree-icon .icon { width: 13px; height: 13px; }
1439.tree-chevron { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); transition: transform 0.12s ease; }
1440.tree-chevron .icon { width: 12px; height: 12px; }
1441details[open] > summary .tree-chevron { transform: rotate(90deg); }
1442.tree-file { padding-left: 25px; }
1443.tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1444.tree-note, .tree-empty { margin: 0; padding: 8px 10px; font-size: calc(12.1px * var(--font-scale)); }
1445.tree-note { border-top: 1px solid var(--border); }
1446
1447/* Review files: the changed-file tree reuses the code browser's shape, with a
1448 status mark in place of the file icon. */
1449.review-tree .tree-file { padding-left: 7px; gap: 7px; }
1450.review-mark {
1451 display: inline-grid;
1452 place-items: center;
1453 flex: 0 0 auto;
1454 width: 16px;
1455 height: 16px;
1456 border-radius: 3px;
1457 background: var(--bg-active);
1458 color: var(--fg-muted);
1459 font-family: var(--mono);
1460 font-size: calc(9.9px * var(--font-scale));
1461 font-weight: 600;
1462 line-height: 1;
1463}
1464.review-mark[data-status="A"] { color: var(--good); background: var(--good-soft); }
1465.review-mark[data-status="D"] { color: var(--bad); background: var(--bad-soft); }
1466.review-mark[data-status="R"] { color: var(--accent); background: var(--accent-soft); }
1467.review-mark[data-status="M"] { color: var(--warn); background: var(--warn-soft); }
1468.review-main { min-width: 0; scroll-margin-top: 16px; }
1469/* A single file is the whole pane, so its header is the pane's top bar: the file
1470 at one end and its counts at the other, as tall as the tree header so both
1471 columns start on the same line. The extra pixel is the section's own top
1472 border, which the height must not add to. */
1473.review-main .diff { margin-top: 0; }
1474.review-main .diff h3 { height: 35px; padding: 0 10px; }
1475
1476.code-main .filebody { max-width: none; }
1477.code-main .readme > *,
1478.code-main .filebody > * { max-width: none; }
1479.empty-code { margin-top: 0; }
1480
1481.dirlist td { padding: 2px 10px; }
1482.dirlist a { display: inline-flex; align-items: center; gap: 7px; }
1483.dirlist .icon { width: 13px; height: 13px; color: var(--fg-faint); }
1484.dirlist a.dir .icon { color: var(--accent); }
1485.dirlist a.parent { color: var(--fg-muted); }
1486
1487@media (max-width: 980px) {
1488 .code-browser { grid-template-columns: 220px minmax(0, 1fr); }
1489}
1490@media (max-width: 760px) {
1491 .code-browser { grid-template-columns: 1fr; }
1492 .file-tree { position: static; max-height: 260px; }
1493}
1494
1495.readme > *,
1496.filebody > * { max-width: 84ch; }
1497.readme > pre,
1498.readme > table,
1499.filebody > pre,
1500.filebody > table { max-width: 100%; }
1501
1502.filebody {
1503 background: var(--bg-panel);
1504 border: 1px solid var(--border);
1505 border-radius: var(--radius-lg);
1506 padding: 18px 20px;
1507 margin: 12px 0 20px;
1508}
1509.filebody > :first-child { margin-top: 0; }
1510
1511/* The source card grows to the file's full height for the same reason: find has
1512 one scroll area, the page. Only a line too wide for the pane scrolls here. */
1513.blob {
1514 overflow-x: auto;
1515}
1516.blob table.code {
1517 font-family: var(--mono);
1518 font-size: calc(12.65px * var(--font-scale));
1519 border-collapse: collapse;
1520}
1521.blob td {
1522 border: 0;
1523 padding: 0 12px;
1524 line-height: 19px;
1525 white-space: pre;
1526 vertical-align: top;
1527}
1528.blob td.ln {
1529 width: 3.5rem;
1530 text-align: right;
1531 color: var(--fg-faint);
1532 user-select: none;
1533 border-right: 1px solid var(--border);
1534 padding: 0 8px;
1535}
1536.blob td.code { padding-left: 12px; }
1537.blob tr:hover { background: none; }
1538
1539/* ---------- syntax highlighting ---------- */
1540
1541/* static/prism-worker.js writes Prism token classes into blob and diff cells
1542 after the page paints. The palette is the same one the rest of the surface
1543 uses, so a token is a colour, never a font switch except for comments and
1544 italics. */
1545
1546:is(.blob, .diff) .token.comment,
1547:is(.blob, .diff) .token.prolog,
1548:is(.blob, .diff) .token.doctype,
1549:is(.blob, .diff) .token.cdata { color: #6f7682; font-style: italic; }
1550:is(.blob, .diff) .token.punctuation { color: #a6accd; }
1551:is(.blob, .diff) .token.namespace { color: #a6accd; }
1552:is(.blob, .diff) .token.operator,
1553:is(.blob, .diff) .token.entity,
1554:is(.blob, .diff) .token.url { color: #7fd3e8; }
1555:is(.blob, .diff) .token.boolean,
1556:is(.blob, .diff) .token.number,
1557:is(.blob, .diff) .token.constant,
1558:is(.blob, .diff) .token.symbol { color: #f2a06b; }
1559:is(.blob, .diff) .token.string,
1560:is(.blob, .diff) .token.char,
1561:is(.blob, .diff) .token.attr-value,
1562:is(.blob, .diff) .token.regex { color: #a8d98a; }
1563:is(.blob, .diff) .token.atrule,
1564:is(.blob, .diff) .token.keyword,
1565:is(.blob, .diff) .token.tag,
1566:is(.blob, .diff) .token.selector { color: #c792ea; }
1567:is(.blob, .diff) .token.function,
1568:is(.blob, .diff) .token.function-name,
1569:is(.blob, .diff) .token.method { color: #83aaff; }
1570:is(.blob, .diff) .token.class-name,
1571:is(.blob, .diff) .token.builtin,
1572:is(.blob, .diff) .token.attr-name,
1573:is(.blob, .diff) .token.property,
1574:is(.blob, .diff) .token.title { color: #f0c674; }
1575:is(.blob, .diff) .token.variable { color: #e7e8ea; }
1576:is(.blob, .diff) .token.deleted { color: var(--bad); }
1577:is(.blob, .diff) .token.inserted { color: var(--good); }
1578:is(.blob, .diff) .token.important,
1579:is(.blob, .diff) .token.bold { font-weight: 600; }
1580:is(.blob, .diff) .token.italic { font-style: italic; }
1581
1582/* ---------- repository overview sidebar ---------- */
1583
1584/* Compact commit and run rows beside the overview. */
1585
1586.side-commit,
1587.side-run,
1588.side-issue,
1589.side-change {
1590 display: flex;
1591 flex-direction: column;
1592 gap: 1px;
1593 padding: 4px 0;
1594 color: var(--fg);
1595 min-width: 0;
1596}
1597.side-commit:hover,
1598.side-run:hover,
1599.side-issue:hover,
1600.side-change:hover { color: var(--accent); text-decoration: none; }
1601.side-subject {
1602 font-size: calc(13.75px * var(--font-scale));
1603 overflow: hidden;
1604 text-overflow: ellipsis;
1605 white-space: nowrap;
1606}
1607.side-num { color: var(--fg-faint); font-variant-numeric: tabular-nums; margin-right: 4px; }
1608.side-meta {
1609 display: flex;
1610 gap: 8px;
1611 font-size: calc(12.1px * var(--font-scale));
1612 color: var(--fg-faint);
1613 white-space: nowrap;
1614}
1615.side-run-top { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
1616.side-run-top .status { flex: 0 0 auto; }
1617
1618/* The Issues section of a change lists its linked issues in the overview's compact
1619 shape, with the remove control beside the row and the add field beneath. */
1620.request-issue { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
1621.request-issue .side-issue { flex: 1; min-width: 0; }
1622.thread-side .request-issue form { flex: 0 0 auto; margin: 0; }
1623.request-issue form button { color: var(--fg-faint); }
1624.request-issue form button:hover { color: var(--bad); }
1625.request-unlink { display: inline-grid; place-items: center; width: 22px; min-height: 22px; border-radius: var(--radius); }
1626.request-unlink:hover { background: var(--bg-hover); text-decoration: none; }
1627#request-combined[hidden], #request-merge-note[hidden] { display: none; }
1628.request-issue-picker { position: relative; margin-top: 8px; }
1629.thread-side .request-issue-picker form { margin: 0; }
1630.request-issue-search input { width: 100%; font-size: calc(12.65px * var(--font-scale)); }
1631.request-issue-results { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40; }
1632.request-issue-popover {
1633 max-height: 18rem;
1634 overflow-y: auto;
1635 padding: 4px;
1636 border: 1px solid var(--border-strong);
1637 border-radius: var(--radius-lg);
1638 background: var(--bg-raised);
1639 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
1640}
1641.request-issue-popover p { margin: 0; padding: 5px 6px; font-size: calc(12.65px * var(--font-scale)); }
1642.request-issue-matches { display: flex; flex-direction: column; gap: 2px; }
1643.thread-side .request-issue-match { margin: 0; }
1644.request-issue-match button {
1645 display: flex;
1646 flex-direction: column;
1647 align-items: stretch;
1648 justify-content: flex-start;
1649 gap: 1px;
1650 width: 100%;
1651 min-height: 0;
1652 padding: 4px 6px;
1653 border: 0;
1654 border-radius: var(--radius);
1655 background: none;
1656 color: var(--fg);
1657 font-weight: 400;
1658 text-align: left;
1659 white-space: normal;
1660}
1661.request-issue-match button:hover { background: var(--bg-hover); color: var(--accent); }
1662
1663/* ---------- run detail ---------- */
1664
1665/* A run's summary is a row of facts, not a paragraph: how long it took, how
1666 long it waited, what it ran on, which commit it tested, and the workflow
1667 definition it ran. */
1668.run-summary {
1669 display: grid;
1670 grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
1671 gap: 10px;
1672 margin: 16px 0;
1673}
1674.run-summary .card {
1675 margin: 0;
1676 display: flex;
1677 flex-direction: column;
1678 gap: 3px;
1679 min-width: 0;
1680}
1681.run-summary-label {
1682 display: flex;
1683 align-items: center;
1684 gap: 5px;
1685 color: var(--fg-faint);
1686 font-size: calc(11.55px * var(--font-scale));
1687 font-weight: 500;
1688 letter-spacing: 0.04em;
1689 text-transform: uppercase;
1690}
1691.run-summary-label .icon { width: 12px; height: 12px; }
1692.run-summary strong {
1693 font-size: calc(17.6px * var(--font-scale));
1694 font-weight: 600;
1695 font-variant-numeric: tabular-nums;
1696 overflow-wrap: anywhere;
1697}
1698.run-summary .status-note { margin: 0; }
1699.run-summary code { font-size: calc(11.55px * var(--font-scale)); overflow-wrap: anywhere; }
1700
1701/* Step logs are loaded on demand. The details element is the trigger; the pre is
1702 the target, and htmx uses textContent so a log can never become markup. */
1703.run-log {
1704 margin: 4px 0;
1705 background: var(--bg-panel);
1706 border: 1px solid var(--border);
1707 border-radius: var(--radius);
1708 overflow: hidden;
1709}
1710.run-log > summary {
1711 display: flex;
1712 align-items: center;
1713 gap: 6px;
1714 padding: 6px 10px;
1715 color: var(--fg-muted);
1716 font-size: calc(13.2px * var(--font-scale));
1717 cursor: pointer;
1718 list-style: none;
1719}
1720.run-log > summary::-webkit-details-marker { display: none; }
1721.run-log > summary:hover { color: var(--fg); background: var(--bg-hover); }
1722.run-log > summary .icon { width: 12px; height: 12px; color: var(--fg-faint); }
1723.run-log[open] > summary {
1724 color: var(--fg);
1725 border-bottom: 1px solid var(--border);
1726}
1727.run-log-card {
1728 padding: 0;
1729 overflow: hidden;
1730}
1731.run-log-card > .run-log {
1732 margin: 0;
1733 border: 0;
1734 border-bottom: 1px solid var(--border);
1735 border-radius: 0;
1736}
1737.run-log-card > .run-log:last-child { border-bottom: 0; }
1738.run-log-title { flex: 1; color: var(--fg); font-weight: 500; }
1739.run-log-meta {
1740 margin-left: auto;
1741 color: var(--fg-faint);
1742 font-size: calc(12.1px * var(--font-scale));
1743 font-variant-numeric: tabular-nums;
1744}
1745.run-step-num {
1746 color: var(--fg-faint);
1747 font-variant-numeric: tabular-nums;
1748 min-width: 2.5ch;
1749}
1750.run-step-name {
1751 flex: 1;
1752 color: var(--fg);
1753 overflow-wrap: anywhere;
1754}
1755.run-step-meta {
1756 margin-left: 8px;
1757 color: var(--fg-faint);
1758 font-size: calc(12.1px * var(--font-scale));
1759 font-variant-numeric: tabular-nums;
1760 white-space: nowrap;
1761}
1762.run-log-empty {
1763 margin: 0;
1764 padding: 10px 12px;
1765 color: var(--fg-muted);
1766 font-size: calc(13.2px * var(--font-scale));
1767}
1768.run-log-text {
1769 margin: 0;
1770 padding: 10px 12px;
1771 border: 0;
1772 border-radius: 0;
1773 background: var(--bg);
1774 color: var(--fg);
1775 font-size: calc(13.2px * var(--font-scale));
1776 line-height: 1.5;
1777 max-height: 34rem;
1778 min-height: 2.5rem;
1779 overflow: auto;
1780 white-space: pre;
1781}
1782.run-logbar {
1783 display: flex;
1784 flex-wrap: wrap;
1785 align-items: center;
1786 gap: 10px;
1787 margin: 8px 0;
1788 font-size: calc(13.2px * var(--font-scale));
1789}
1790
1791/* ---------- issues and changes ---------- */
1792
1793.page-head {
1794 display: flex;
1795 align-items: flex-end;
1796 justify-content: space-between;
1797 gap: 24px;
1798 padding: 26px 0 20px;
1799 margin-bottom: 20px;
1800 border-bottom: 1px solid var(--border);
1801}
1802.page-head h1 {
1803 font-size: calc(28.6px * var(--font-scale));
1804 line-height: 1.2;
1805 letter-spacing: -0.025em;
1806 margin: 0 0 4px;
1807}
1808.page-head-main { min-width: 0; }
1809/* The `#42` alias leads a page heading, the way it leads a row in a list: the
1810 number is what a reader quotes, and the title is what they are looking for. */
1811.page-num {
1812 color: var(--fg-faint);
1813 font-variant-numeric: tabular-nums;
1814 margin-right: 7px;
1815}
1816.page-head-actions {
1817 display: flex;
1818 flex-wrap: wrap;
1819 flex: 0 0 auto;
1820 align-items: center;
1821 justify-content: flex-end;
1822 gap: 8px;
1823 margin-left: auto;
1824}
1825/* Search and branch pickers are filters, not inline prose: a row layout keeps the
1826 input, its icon button, and the select on the same baseline. */
1827.page-head-actions form.inline {
1828 display: inline-flex;
1829 align-items: center;
1830 gap: 6px;
1831}
1832.page-sub { color: var(--fg-muted); font-size: calc(14.3px * var(--font-scale)); margin: 0; max-width: 80ch; }
1833
1834@media (max-width: 720px) {
1835 .page-head { padding: 18px 0 16px; }
1836 .page-head h1 { font-size: calc(24.2px * var(--font-scale)); }
1837}
1838
1839/* Secondary navigation inside a change: conversation and the review files. It is
1840 the same `.tabrow` and `.tabs` as every other row, pulled up against the
1841 request header. */
1842.subtabs { margin-top: -2px; }
1843
1844.state {
1845 display: inline-flex;
1846 align-items: center;
1847 gap: 5px;
1848 padding: 1px 7px;
1849 border: 1px solid var(--border-strong);
1850 border-radius: 10px;
1851 font-size: calc(11.55px * var(--font-scale));
1852 line-height: 1.6;
1853 font-weight: 500;
1854 text-transform: uppercase;
1855 letter-spacing: 0.04em;
1856 color: var(--fg-muted);
1857 white-space: nowrap;
1858}
1859.state::before {
1860 content: "";
1861 width: 6px;
1862 height: 6px;
1863 border-radius: 50%;
1864 background: currentColor;
1865}
1866.state.open {
1867 color: var(--good);
1868 border-color: color-mix(in srgb, var(--good) 45%, var(--border));
1869 background: var(--good-soft);
1870}
1871.state.closed { color: var(--fg-faint); }
1872.state.merged {
1873 color: var(--merged);
1874 border-color: color-mix(in srgb, var(--merged) 45%, var(--border));
1875 background: var(--merged-soft);
1876}
1877
1878.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
1879.chip-count { margin-left: 5px; color: var(--fg-faint); font-variant-numeric: tabular-nums; }
1880.chip.active .chip-count { color: var(--accent); }
1881.filter-label {
1882 color: var(--fg-faint);
1883 font-size: calc(12.1px * var(--font-scale));
1884 text-transform: uppercase;
1885 letter-spacing: 0.04em;
1886}
1887
1888.row-title { font-weight: 550; color: var(--fg); }
1889.row-title:hover { color: var(--accent); text-decoration: none; }
1890.row-meta {
1891 display: flex;
1892 flex-wrap: wrap;
1893 align-items: center;
1894 gap: 4px 8px;
1895 margin-top: 2px;
1896 font-size: calc(12.65px * var(--font-scale));
1897 color: var(--fg-faint);
1898}
1899
1900/* The one word a list says about where a row is: a change's waiting state, or an
1901 issue's open/closed. The icon carries the colour, the label the meaning, and,
1902 where there is one, the title attribute the reason. */
1903.row-status {
1904 display: inline-flex;
1905 align-items: center;
1906 gap: 5px;
1907 font-size: calc(12.65px * var(--font-scale));
1908 color: var(--fg-muted);
1909 white-space: nowrap;
1910}
1911.row-status .icon { width: 13px; height: 13px; flex: 0 0 auto; }
1912.row-status.open,
1913.row-status.ready { color: var(--good); }
1914.row-status.merged { color: var(--merged); }
1915.row-status.failed,
1916.row-status.conflict { color: var(--bad); }
1917.row-status.waiting,
1918.row-status.review { color: var(--warn); }
1919.row-status.closed { color: var(--fg-faint); }
1920
1921/* A search snippet is the sentence the query matched, with the matched words
1922 marked. It sits under the row title, quieter than the title and smaller than
1923 the body it came from. */
1924.search-snippet {
1925 margin: 3px 0 0;
1926 font-size: calc(12.65px * var(--font-scale));
1927 color: var(--fg-faint);
1928}
1929.search-snippet mark {
1930 background: var(--accent-soft);
1931 color: var(--fg);
1932 border-radius: 2px;
1933 padding: 0 1px;
1934}
1935
1936/* An empty state stands in for a list or a pane of content that has nothing to
1937 show. Use it for a whole region, not for one quiet line inside a card, a
1938 sidebar, a tree, or a table cell: those stay .muted so a page does not fill
1939 with dashed boxes. */
1940.empty-state {
1941 border: 1px dashed var(--border-strong);
1942 border-radius: var(--radius-lg);
1943 padding: 20px;
1944 margin: 14px 0;
1945 color: var(--fg-muted);
1946}
1947.empty-state p { margin: 0 0 8px; }
1948.empty-state p:last-child { margin-bottom: 0; }
1949.empty-state pre { margin: 8px 0; }
1950
1951/* Thread layout: conversation in the main column, action and metadata beside it. */
1952.thread {
1953 display: grid;
1954 grid-template-columns: minmax(0, 1fr) 280px;
1955 gap: 24px;
1956 align-items: start;
1957}
1958.thread-main { min-width: 0; }
1959.thread-side section { padding: 12px 0 14px; border-bottom: 1px solid var(--border); }
1960.thread-side #request-sidebar > #request-review { padding-top: 0; }
1961.thread-side > section:last-child,
1962.thread-side #request-sidebar > section:last-child { border-bottom: 0; }
1963.thread-side h2 { margin: 0 0 8px; }
1964.thread-side form { margin: 8px 0 0; }
1965.thread-side .meta-list { margin-bottom: 0; }
1966
1967/* The run rows beside a change use the same compact shape as the recent runs
1968 on the overview: status and job, then the context under them. */
1969.thread-side .side-meta { flex-wrap: wrap; white-space: normal; }
1970.thread-side .side-meta > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1971
1972/* A `?` next to a section heading: the explanation is a tooltip rather than a
1973 paragraph, so the panel keeps only the state it is about. The tooltip opens
1974 to the left because the sidebar sits at the right edge of the page. */
1975.help {
1976 position: relative;
1977 display: inline-grid;
1978 place-items: center;
1979 width: 15px;
1980 height: 15px;
1981 margin-left: 6px;
1982 border: 1px solid var(--border-strong);
1983 border-radius: 50%;
1984 color: var(--fg-faint);
1985 font-family: var(--font);
1986 font-size: calc(10px * var(--font-scale));
1987 font-weight: 600;
1988 line-height: 1;
1989 vertical-align: 1px;
1990 cursor: help;
1991 user-select: none;
1992}
1993.help:hover,
1994.help:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }
1995.help::after,
1996.contributors .contrib-stat::after {
1997 content: attr(data-tip);
1998 position: absolute;
1999 top: 50%;
2000 right: calc(100% + 8px);
2001 z-index: 20;
2002 width: max-content;
2003 max-width: 210px;
2004 padding: 7px 9px;
2005 border: 1px solid var(--border-strong);
2006 border-radius: var(--radius);
2007 background: var(--bg-raised);
2008 color: var(--fg);
2009 font-size: calc(12.1px * var(--font-scale));
2010 font-weight: 400;
2011 line-height: 1.45;
2012 letter-spacing: normal;
2013 text-align: left;
2014 white-space: normal;
2015 transform: translateY(-50%);
2016 opacity: 0;
2017 visibility: hidden;
2018 pointer-events: none;
2019 box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
2020}
2021.help:hover::after,
2022.help:focus::after,
2023.help:focus-visible::after,
2024.contributors .contrib-stat:hover::after,
2025.contributors .contrib-stat:focus::after,
2026.contributors .contrib-stat:focus-visible::after { opacity: 1; visibility: visible; }
2027@media (prefers-reduced-motion: no-preference) {
2028 .help::after { transition: opacity 120ms ease; }
2029}
2030
2031/* Scope the card to the article that renders it. Prism calls a syntax token
2032 "comment" too, and an unscoped .comment rule put a rounded panel behind every
2033 comment-coloured line in a blob or diff. */
2034article.comment {
2035 background: var(--bg-panel);
2036 border: 1px solid var(--border);
2037 border-radius: var(--radius-lg);
2038 margin: 10px 0;
2039 overflow: visible;
2040 scroll-margin-top: 16px;
2041}
2042article.comment:target {
2043 border-color: var(--accent);
2044 box-shadow: 0 0 0 2px var(--accent-soft);
2045}
2046article.comment:target .comment-head { background: var(--accent-soft); }
2047.comment-head {
2048 display: flex;
2049 flex-wrap: wrap;
2050 align-items: center;
2051 gap: 6px 8px;
2052 padding: 7px 12px;
2053 background: var(--bg-raised);
2054 border-bottom: 1px solid var(--border);
2055 border-top-left-radius: calc(var(--radius-lg) - 1px);
2056 border-top-right-radius: calc(var(--radius-lg) - 1px);
2057 font-size: calc(13.2px * var(--font-scale));
2058}
2059.comment-author { font-weight: 550; }
2060.comment-head .muted { font-size: calc(12.65px * var(--font-scale)); }
2061.reply-context { color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); }
2062.reply-context a { color: var(--fg-muted); }
2063.reply-context a:hover { color: var(--accent); }
2064.comment-body { padding: 12px 14px; }
2065.comment-body > :first-child { margin-top: 0; }
2066.comment-body > :last-child { margin-bottom: 0; }
2067
2068article.comment.deleted {
2069 border-style: dashed;
2070 background: transparent;
2071}
2072.comment-deleted {
2073 display: flex;
2074 align-items: center;
2075 gap: 6px;
2076 padding: 10px 14px;
2077 color: var(--fg-faint);
2078 font-size: calc(13.2px * var(--font-scale));
2079 font-style: italic;
2080}
2081.comment-deleted .icon { width: 13px; height: 13px; }
2082
2083/* Replies are displayed one level deep: a root comment leads, all its
2084 descendants sit in an indented rail, and the reply composer joins the thread. */
2085.comment-replies {
2086 display: flex;
2087 flex-direction: column;
2088 gap: 8px;
2089 margin: 0 0 12px 26px;
2090 padding-left: 12px;
2091 border-left: 2px solid var(--border);
2092}
2093.comment-replies article.comment {
2094 margin: 0;
2095 background: var(--bg-raised);
2096}
2097.comment-menu { position: relative; margin-left: auto; }
2098.comment-menu-toggle {
2099 display: inline-flex;
2100 align-items: center;
2101 justify-content: center;
2102 width: 22px;
2103 min-height: 0;
2104 padding: 0;
2105 border: 0;
2106 border-radius: 4px;
2107 background: none;
2108 color: var(--fg-muted);
2109 font-family: var(--font);
2110 font-size: calc(17.6px * var(--font-scale));
2111 font-weight: 600;
2112 line-height: 1;
2113 cursor: pointer;
2114}
2115.comment-menu-toggle:hover { background: var(--bg-hover); color: var(--fg); }
2116.comment-menu-list {
2117 position: absolute;
2118 z-index: 60;
2119 top: calc(100% + 4px);
2120 right: 0;
2121 min-width: 10rem;
2122 padding: 4px;
2123 border: 1px solid var(--border-strong);
2124 border-radius: var(--radius);
2125 background: var(--bg-raised);
2126 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
2127}
2128.comment-menu-list[hidden] { display: none; }
2129.comment-menu-list button {
2130 display: block;
2131 width: 100%;
2132 min-height: 0;
2133 padding: 6px 9px;
2134 border: 0;
2135 border-radius: 4px;
2136 background: none;
2137 color: var(--fg);
2138 text-align: left;
2139 font-family: var(--font);
2140 font-size: calc(13.2px * var(--font-scale));
2141 font-weight: 400;
2142 line-height: 1.4;
2143 cursor: pointer;
2144}
2145.comment-menu-list button:hover { background: var(--bg-hover); }
2146.comment-menu-list form { margin: 0; }
2147.comment-menu-list .menu-danger { color: var(--bad); }
2148.comment-menu-list .menu-danger:hover { background: var(--bad-soft); }
2149.comment-edit-form, .details-edit-form { padding: 10px 14px 12px; }
2150.comment-edit-form[hidden], .details-edit-form[hidden] { display: none; }
2151.comment-edit-form textarea, .details-edit-form textarea { min-height: 7rem; max-width: none; }
2152.comment-edit-form .form-actions, .details-edit-form .form-actions { margin-top: 8px; }
2153article.comment.editing > .comment-body,
2154article.comment.editing > .comment-snippet { display: none; }
2155
2156/* The heading is one row: the number stays put, the title sits beside it, and
2157 the pencil opens a title-only form that takes the title's place. */
2158.details-title { display: flex; align-items: baseline; flex-wrap: wrap; gap: 7px; margin: 0 0 4px; }
2159.details-title .page-num { flex: 0 0 auto; margin-right: 0; }
2160.details-title-view { min-width: 0; margin: 0; }
2161.details-title-view[hidden], .title-edit-form[hidden], .title-edit-toggle[hidden] { display: none; }
2162.title-edit-toggle {
2163 display: inline-grid;
2164 place-items: center;
2165 flex: 0 0 auto;
2166 width: 22px;
2167 height: 22px;
2168 border-radius: var(--radius);
2169 color: var(--fg-faint);
2170}
2171.title-edit-toggle:hover { background: var(--bg-hover); color: var(--accent); text-decoration: none; }
2172.title-edit-toggle .icon { width: 14px; height: 14px; }
2173.title-edit-form { display: flex; align-items: center; flex: 1 1 18rem; gap: 6px; min-width: 0; }
2174.title-edit-form .details-title-input { flex: 1 1 auto; min-width: 8rem; }
2175.title-edit-form button { flex: 0 0 auto; }
2176.details-title-input {
2177 width: 100%;
2178 max-width: none;
2179 font-family: var(--font);
2180 font-size: calc(19.8px * var(--font-scale));
2181 font-weight: 600;
2182 line-height: 1.35;
2183 letter-spacing: -0.005em;
2184}
2185
2186/* A thread keeps its root visible and lets the replies fold away. */
2187.thread-replies { margin: 0 0 10px 26px; }
2188.thread-replies > summary {
2189 display: inline-flex;
2190 align-items: center;
2191 gap: 6px;
2192 width: max-content;
2193 padding: 2px 0;
2194 color: var(--fg-muted);
2195 font-size: calc(12.65px * var(--font-scale));
2196 cursor: pointer;
2197 list-style: none;
2198}
2199.thread-replies > summary::-webkit-details-marker { display: none; }
2200.thread-replies > summary:hover { color: var(--accent); }
2201.thread-replies[open] > summary { margin-bottom: 6px; }
2202.thread-chevron { display: inline-flex; color: var(--fg-faint); transition: transform 0.12s ease; }
2203.thread-chevron .icon { width: 12px; height: 12px; }
2204.thread-replies[open] .thread-chevron { transform: rotate(90deg); }
2205.thread-replies .comment-replies { margin: 6px 0 0; }
2206
2207.discussion-composer { margin: 16px 0 0; }
2208.discussion-composer textarea {
2209 width: 100%;
2210 max-width: none;
2211 min-height: 7rem;
2212 padding: 12px 14px;
2213 background: var(--bg-panel);
2214 border-color: var(--border);
2215 border-radius: var(--radius-lg);
2216 line-height: 1.55;
2217}
2218.discussion-composer .form-actions { margin-top: 8px; }
2219.reply-target {
2220 display: flex;
2221 align-items: center;
2222 justify-content: space-between;
2223 gap: 8px;
2224 margin: 0 0 8px;
2225 padding: 5px 8px;
2226 border: 1px solid var(--accent);
2227 border-radius: var(--radius);
2228 background: var(--accent-soft);
2229 color: var(--fg-muted);
2230 font-size: calc(12.65px * var(--font-scale));
2231}
2232.reply-target[hidden] { display: none; }
2233.reply-target strong { color: var(--fg); }
2234
2235/* An anchored comment can show the lines it was written about. The details
2236 element keeps the conversation compact. */
2237.comment-snippet {
2238 margin: 0 14px 12px;
2239 border: 1px solid var(--border);
2240 border-radius: var(--radius);
2241 background: var(--bg-raised);
2242 overflow: hidden;
2243}
2244.comment-snippet > summary {
2245 display: flex;
2246 align-items: center;
2247 flex-wrap: wrap;
2248 gap: 4px 8px;
2249 padding: 7px 10px;
2250 color: var(--fg-muted);
2251 font-size: calc(12.65px * var(--font-scale));
2252 cursor: pointer;
2253 list-style: none;
2254}
2255.comment-snippet > summary::-webkit-details-marker { display: none; }
2256.comment-snippet > summary:hover { background: var(--bg-hover); }
2257.comment-snippet code { color: var(--fg); font-family: var(--mono); }
2258.comment-snippet[open] > summary { border-bottom: 1px solid var(--border); }
2259.snippet-chevron { display: inline-flex; color: var(--fg-faint); transition: transform 0.12s ease; }
2260.snippet-chevron .icon { width: 12px; height: 12px; }
2261.comment-snippet[open] .snippet-chevron { transform: rotate(90deg); }
2262.snippet-code {
2263 overflow-x: auto;
2264 background: var(--bg-panel);
2265 font-family: var(--mono);
2266 font-size: calc(12.65px * var(--font-scale));
2267 line-height: 1.55;
2268}
2269.snippet-line { display: flex; min-width: max-content; }
2270.snippet-line.anchor { background: var(--accent-soft); }
2271.snippet-no {
2272 flex: 0 0 3.2rem;
2273 padding-right: 8px;
2274 text-align: right;
2275 color: var(--fg-faint);
2276 user-select: none;
2277 border-right: 1px solid var(--border);
2278}
2279.snippet-text { padding: 0 10px; white-space: pre; color: var(--fg); }
2280.snippet-actions {
2281 padding: 6px 10px;
2282 border-top: 1px solid var(--border);
2283 font-size: calc(12.65px * var(--font-scale));
2284}
2285
2286.meta-list {
2287 display: grid;
2288 grid-template-columns: auto minmax(0, 1fr);
2289 gap: 4px 10px;
2290 margin: 0;
2291 font-size: calc(13.2px * var(--font-scale));
2292}
2293.meta-list dt { color: var(--fg-faint); }
2294.meta-list dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
2295
2296/* The request target is two stacked fields, not one crowded row: the sidebar is
2297 narrow, so each branch name gets the full width and a visible label. */
2298.request-target { margin: 2px 0 0; }
2299.target-fields { display: grid; gap: 4px; }
2300.target-field { display: grid; gap: 3px; min-width: 0; margin: 0; }
2301.target-label { color: var(--fg-muted); font-size: calc(12.1px * var(--font-scale)); }
2302.request-target select {
2303 min-width: 0;
2304 width: 100%;
2305 min-height: 30px;
2306 padding: 4px 7px;
2307 font-size: calc(13.2px * var(--font-scale));
2308}
2309.target-actions {
2310 display: flex;
2311 flex-wrap: wrap;
2312 align-items: center;
2313 gap: 6px 10px;
2314 margin-top: 6px;
2315}
2316.target-actions button {
2317 display: inline-flex;
2318 align-items: center;
2319 justify-content: center;
2320 min-height: 28px;
2321 margin-left: auto;
2322 padding: 3px 9px;
2323}
2324.request-target p { margin: 0; font-size: calc(12.65px * var(--font-scale)); }
2325.target-summary {
2326 display: flex;
2327 flex-wrap: wrap;
2328 align-items: center;
2329 gap: 5px;
2330 margin: 0;
2331 font-size: calc(13.2px * var(--font-scale));
2332}
2333.label-row { display: inline-flex; flex-wrap: wrap; gap: 4px; }
2334.label-row .label { margin-left: 0; }
2335
2336.approval-list {
2337 list-style: none;
2338 padding: 0;
2339 margin: 0;
2340 display: flex;
2341 flex-direction: column;
2342 gap: 6px;
2343 font-size: calc(13.2px * var(--font-scale));
2344}
2345.approval-list li { display: flex; align-items: center; gap: 7px; min-width: 0; }
2346.approval-list .cname {
2347 flex: 1;
2348 min-width: 0;
2349 overflow: hidden;
2350 text-overflow: ellipsis;
2351 white-space: nowrap;
2352}
2353.approval-list .muted { font-size: calc(12.1px * var(--font-scale)); }
2354
2355@media (max-width: 900px) {
2356 .thread { grid-template-columns: 1fr; }
2357 .thread-side { display: block; }
2358 .thread-side #request-aside { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 24px; }
2359 .thread-side #request-sidebar { display: contents; }
2360 .thread-side section:first-child { padding-top: 12px; }
2361 .help::after,
2362 .contributors .contrib-stat::after { top: calc(100% + 7px); right: auto; left: 0; transform: none; }
2363 .page-head { flex-wrap: wrap; }
2364 .page-head-actions { width: 100%; justify-content: flex-start; }
2365}
2366
2367/* Form cards: fields live on a card, actions sit below it and align right.
2368 Every primary action uses the same height and horizontal padding. */
2369.form-block { margin: 12px 0; }
2370.danger-form + .danger-form { margin-top: 28px; }
2371.acl-check { display: flex; align-items: center; gap: 6px; }
2372.acl-check input[type="checkbox"] { width: auto; margin: 0; }
2373table.acl select { min-width: 9rem; }
2374.role-options { border: 0; padding: 0; margin: 0; min-width: 0; }
2375.role-options legend {
2376 padding: 0;
2377 margin: 0 0 8px;
2378 color: var(--fg);
2379 font-size: calc(14.3px * var(--font-scale));
2380 font-weight: 600;
2381}
2382.role-options > p { margin: 0 0 10px; }
2383.role-option {
2384 display: flex;
2385 align-items: baseline;
2386 gap: 7px;
2387 margin: 5px 0;
2388 padding: 7px 10px;
2389 border: 1px solid var(--border);
2390 border-radius: var(--radius);
2391 color: var(--fg);
2392 font-size: calc(14.3px * var(--font-scale));
2393 cursor: pointer;
2394}
2395.role-option:hover { border-color: var(--border-strong); background: var(--bg-raised); }
2396.role-option:has(input[type="radio"]:checked) {
2397 border-color: var(--accent);
2398 background: var(--accent-soft);
2399}
2400.role-option input[type="radio"] {
2401 display: block;
2402 flex: 0 0 auto;
2403 width: auto;
2404 margin: 0;
2405 accent-color: var(--accent);
2406}
2407.role-option .role-name { font-weight: 600; }
2408.role-option .role-desc { color: var(--fg-muted); font-size: calc(13.2px * var(--font-scale)); line-height: 1.45; }
2409.role-options.compact { display: flex; flex-wrap: wrap; gap: 4px 12px; }
2410.role-options.compact legend { display: none; }
2411.role-options.compact .role-option {
2412 display: inline-flex;
2413 align-items: center;
2414 gap: 5px;
2415 margin: 0;
2416 padding: 0;
2417 border: 0;
2418 border-radius: 0;
2419 background: none;
2420 font-size: calc(13.2px * var(--font-scale));
2421}
2422.role-options.compact .role-option:hover { background: none; }
2423.role-options.compact .role-option:has(input[type="radio"]:checked) {
2424 border: 0;
2425 background: none;
2426}
2427.role-options.compact .role-option:has(input[type="radio"]:checked) .role-name { color: var(--accent); }
2428.role-options.compact .role-desc { display: none; }
2429.role-options.compact input[type="radio"] { margin: 0; }
2430.form-card { margin: 0; }
2431.form-actions {
2432 display: flex;
2433 justify-content: flex-end;
2434 align-items: center;
2435 gap: 8px;
2436 margin-top: 8px;
2437}
2438.auth > .card { margin: 0; }
2439.auth > .form-actions { margin-top: 8px; }
2440.form-card input[type="text"],
2441.form-card input[type="password"],
2442.form-card input[type="search"],
2443.form-card textarea,
2444.form-card select {
2445 width: 100%;
2446 max-width: none;
2447}
2448
2449/* The public-key card is the field: the label sits above it and the textarea
2450 fills the card edge to edge. The focus ring draws inside the card so the
2451 rounded frame does not clip it. */
2452.key-card { padding: 0; overflow: hidden; }
2453.key-card textarea {
2454 display: block;
2455 min-height: 8rem;
2456 margin: 0;
2457 padding: 12px 14px;
2458 border: 0;
2459 border-radius: 0;
2460 background: transparent;
2461 resize: vertical;
2462}
2463.key-card textarea:focus {
2464 outline: 2px solid var(--accent);
2465 outline-offset: -2px;
2466 border-color: transparent;
2467}
2468
2469button, .button {
2470 min-height: 30px;
2471 padding-top: 0;
2472 padding-bottom: 0;
2473}
2474button.link, .link { min-height: 0; }
2475
2476/* Label token input: the bordered box is the control, the text input inside it
2477 adds labels, and selected labels are removable chips. */
2478.label-input { position: relative; }
2479.label-tokens {
2480 display: flex;
2481 flex-wrap: wrap;
2482 align-items: center;
2483 gap: 5px;
2484 min-height: 32px;
2485 padding: 3px 8px;
2486 background: var(--bg);
2487 border: 1px solid var(--border-strong);
2488 border-radius: var(--radius);
2489}
2490.label-tokens:focus-within {
2491 border-color: var(--accent);
2492 outline: 2px solid var(--accent-soft);
2493 outline-offset: 1px;
2494}
2495.label-token {
2496 display: inline-flex;
2497 align-items: center;
2498 gap: 3px;
2499 padding: 1px 3px 1px 8px;
2500 border-radius: 10px;
2501 background: var(--accent-soft);
2502 color: var(--accent);
2503 font-size: calc(12.65px * var(--font-scale));
2504 line-height: 1.6;
2505}
2506.label-remove {
2507 min-height: 0;
2508 padding: 0 3px;
2509 background: none;
2510 border: 0;
2511 color: inherit;
2512 font-size: calc(15.4px * var(--font-scale));
2513 line-height: 1;
2514 cursor: pointer;
2515}
2516.label-remove:hover { color: var(--fg); }
2517.label-tokens .label-entry {
2518 flex: 1 1 9rem;
2519 width: auto;
2520 max-width: none;
2521 min-width: 8rem;
2522 margin: 0;
2523 padding: 2px 0;
2524 background: none;
2525 border: 0;
2526 border-radius: 0;
2527 font-size: calc(13.75px * var(--font-scale));
2528 color: var(--fg);
2529}
2530.label-tokens .label-entry:focus { outline: none; }
2531.label-entry::placeholder { color: var(--fg-faint); }
2532.label-suggest {
2533 position: absolute;
2534 z-index: 30;
2535 top: calc(100% + 4px);
2536 left: 0;
2537 right: 0;
2538 max-height: 12rem;
2539 overflow: auto;
2540 padding: 4px;
2541 background: var(--bg-raised);
2542 border: 1px solid var(--border-strong);
2543 border-radius: var(--radius);
2544 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
2545}
2546.label-suggest[hidden], .label-option[hidden] { display: none; }
2547.label-option {
2548 display: block;
2549 width: 100%;
2550 min-height: 0;
2551 padding: 5px 8px;
2552 background: none;
2553 border: 0;
2554 border-radius: 4px;
2555 color: var(--fg);
2556 text-align: left;
2557 font-size: calc(13.2px * var(--font-scale));
2558 font-weight: 400;
2559}
2560.label-option:hover, .label-option:focus { background: var(--bg-hover); filter: none; }
2561
2562/* ---------- icons ---------- */
2563
2564/* Inline SVG from Lucide. Icons inherit currentColor and size with the text
2565 around them. */
2566.icon { width: 14px; height: 14px; flex: 0 0 auto; }
2567.tabs a { display: inline-flex; align-items: center; gap: 5px; }
2568h2 .icon { width: 13px; height: 13px; margin-right: 4px; vertical-align: -2px; color: var(--fg-faint); }
2569
2570/* A token save is triggered by the token input, not a button. Dim the field
2571 while its request is in flight. */
2572form.htmx-request .label-tokens { opacity: 0.6; }
2573
2574/* HTMX's indicator rules live here rather than in the <style> element it would
2575 inject, because the CSP allows style-src 'self' only. Keep them in step with
2576 the config in chrome.html that turns that injection off. */
2577.htmx-indicator { opacity: 0; visibility: hidden; }
2578.htmx-request .htmx-indicator, .htmx-request.htmx-indicator {
2579 opacity: 1;
2580 visibility: visible;
2581 transition: opacity 200ms ease-in;
2582}
2583
2584/* Line-anchored review comments: a small button on each line opens the composer. */
2585.diff td.comment-cell {
2586 padding: 0 3px;
2587 text-align: center;
2588 white-space: nowrap;
2589}
2590.diff td.comment-cell a {
2591 display: inline-flex;
2592 align-items: center;
2593 justify-content: center;
2594 width: 18px;
2595 height: 18px;
2596 border: 0;
2597 background: none;
2598 color: var(--fg-muted);
2599 opacity: 0.55;
2600 transition: opacity 0.12s ease, color 0.12s ease;
2601}
2602.diff tr:hover td.comment-cell a { opacity: 1; color: var(--accent); }
2603.diff td.comment-cell a:focus-visible {
2604 opacity: 1;
2605 color: var(--accent);
2606 outline: 2px solid var(--accent);
2607 outline-offset: 1px;
2608 border-radius: 3px;
2609}
2610.diff td.comment-cell .icon { width: 12px; height: 12px; }
2611.anchor-note { margin: 0 0 8px; font-size: calc(13.2px * var(--font-scale)); }
2612@media (max-width: 720px) { .diff td.comment-cell a { opacity: 0.8; } }
2613
2614/* ---------- activity feed ---------- */
2615.activity-list {
2616 border: 1px solid var(--border);
2617 border-radius: var(--radius-lg);
2618 background: var(--bg-panel);
2619 overflow: hidden;
2620}
2621.activity-event {
2622 display: flex;
2623 gap: 10px;
2624 padding: 10px 12px;
2625 border-bottom: 1px solid var(--border);
2626 align-items: flex-start;
2627}
2628.activity-event:last-child { border-bottom: 0; }
2629.activity-icon {
2630 display: grid;
2631 place-items: center;
2632 flex: 0 0 auto;
2633 width: 24px;
2634 height: 24px;
2635 border-radius: 6px;
2636 background: var(--bg-raised);
2637 color: var(--fg-muted);
2638}
2639.activity-icon .icon { width: 13px; height: 13px; }
2640.activity-icon.issue { color: var(--good); }
2641.activity-icon.change { color: #c792ea; }
2642.activity-icon.release { color: var(--warn); }
2643.activity-body { min-width: 0; flex: 1; }
2644.activity-title {
2645 display: block;
2646 color: var(--fg);
2647 font-weight: 500;
2648 overflow: hidden;
2649 text-overflow: ellipsis;
2650 white-space: nowrap;
2651}
2652.activity-title:hover { color: var(--accent); text-decoration: none; }
2653.activity-meta { color: var(--fg-faint); font-size: calc(12.65px * var(--font-scale)); margin-top: 2px; }
2654@media (max-width: 720px) {
2655 .activity-title { white-space: normal; }
2656}
2657.danger-zone { border-color: color-mix(in srgb, var(--bad) 40%, var(--border)); }
2658.danger-zone h3 { margin: 14px 0 4px; color: var(--fg-muted); }
2659.state.active { color: var(--good); }
2660.state.retired { color: var(--fg-faint); }
2661
2662/* ---------- accessibility ---------- */
2663
2664/* Keyboard users get out of the chrome before the tabs. */
2665.skip-link {
2666 position: absolute;
2667 left: -999px;
2668 top: 8px;
2669 z-index: 100;
2670 padding: 6px 10px;
2671 background: var(--bg-raised);
2672 border: 1px solid var(--accent);
2673 border-radius: var(--radius);
2674}
2675.skip-link:focus { left: 12px; }
2676
2677a:focus-visible,
2678button:focus-visible,
2679select:focus-visible,
2680input:focus-visible,
2681textarea:focus-visible {
2682 outline: 2px solid var(--accent);
2683 outline-offset: 2px;
2684}
2685
2686@media (prefers-reduced-motion: reduce) {
2687 *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
2688 /* The ring still marks the waiting button, but it does not turn. */
2689 .spinner { animation: none; }
2690}
2691
2692@media (max-width: 720px) {
2693 .panel { overflow-x: auto; }
2694 .panel > table { min-width: 34rem; }
2695}
2696
2697/* ---------- light theme ---------- */
2698
2699/* A light theme is the same layout with a second set of tokens. It follows the
2700 operating system until a real theme switch exists. */
2701@media (prefers-color-scheme: light) {
2702 :root {
2703 color-scheme: light;
2704 --bg: #f7f7f8;
2705 --bg-panel: #ffffff;
2706 --bg-raised: #f1f2f4;
2707 --bg-hover: #eceef2;
2708 --bg-active: #e4e7ec;
2709 --border: #dfe2e7;
2710 --border-strong: #c7ccd5;
2711 --fg: #1b1e23;
2712 --fg-muted: #5d646e;
2713 --fg-faint: #858c96;
2714 --accent: #4f5bd5;
2715 --accent-fg: #ffffff;
2716 --accent-soft: rgba(79, 91, 213, 0.12);
2717 --good: #247f55;
2718 --good-soft: rgba(36, 127, 85, 0.12);
2719 --bad: #bd3b3b;
2720 --bad-soft: rgba(189, 59, 59, 0.10);
2721 --warn: #946200;
2722 --warn-soft: rgba(148, 98, 0, 0.12);
2723 --merged: #7b3fb5;
2724 --merged-soft: rgba(123, 63, 181, 0.12);
2725 }
2726}
2727/* ---------- pager ---------- */
2728
2729.pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 12px 0; }
2730.pager .button { font-size: calc(13.2px * var(--font-scale)); }
2731
2732/* ---------- code search ---------- */
2733
2734/* One block per file, with line numbers linked to blob anchors. */
2735.search-group { margin: 14px 0; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-panel); overflow: hidden; }
2736.search-group h3 { margin: 0; padding: 7px 10px; background: var(--bg-raised); border-bottom: 1px solid var(--border); font-size: calc(13.2px * var(--font-scale)); }
2737.search-group h3 a { color: var(--fg); }
2738.search-lines td.code { font-family: var(--mono); font-size: calc(13.2px * var(--font-scale)); white-space: pre-wrap; word-break: break-word; }
2739.search-lines td.ln { width: 5ch; text-align: right; color: var(--fg-faint); font-family: var(--mono); font-size: calc(12.1px * var(--font-scale)); }
2740.search-lines td.ln a { color: var(--fg-faint); }
2741.search-lines tr:hover td.ln a { color: var(--accent); }
2742.code-main tr:target { background: var(--accent-soft); }
2743
2744/* ---------- blame ---------- */
2745
2746/* A commit column spans the consecutive lines it last touched. */
2747.blame-panel { overflow-x: auto; }
2748.blame { min-width: 40rem; }
2749.blame td.blame-meta {
2750 width: 16rem;
2751 border-right: 1px solid var(--border);
2752 background: var(--bg-raised);
2753 vertical-align: top;
2754 font-size: calc(12.65px * var(--font-scale));
2755 line-height: 1.45;
2756}
2757.blame td.blame-meta .sha { font-size: calc(12.1px * var(--font-scale)); }
2758.blame td.ln { width: 6ch; text-align: right; color: var(--fg-faint); font-family: var(--mono); font-size: calc(12.1px * var(--font-scale)); }
2759.blame td.code { font-family: var(--mono); font-size: calc(13.2px * var(--font-scale)); white-space: pre; }
2760
2761/* ---------- landing page ---------- */
2762
2763/* The identity strip, the introduction, the repository grid and the merged feed
2764 as a rail. The feed has a full page at /activity; here it is a sign the forge
2765 is alive, not the subject of the page. */
2766.hero {
2767 display: flex;
2768 align-items: flex-end;
2769 justify-content: space-between;
2770 gap: 24px;
2771 padding: 26px 0 20px;
2772 border-bottom: 1px solid var(--border);
2773 margin-bottom: 20px;
2774}
2775.hero-main { min-width: 0; }
2776.hero h1 {
2777 font-size: calc(28.6px * var(--font-scale));
2778 line-height: 1.2;
2779 letter-spacing: -0.025em;
2780 margin: 0;
2781 overflow: hidden;
2782 text-overflow: ellipsis;
2783 white-space: nowrap;
2784}
2785.hero-actions { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; }
2786
2787/* The introduction sits in the repository column, above the list, so it is as
2788 wide as the work rather than as wide as the page. */
2789.readme-card { margin: 0 0 20px; }
2790.readme-body { padding: 18px 20px; }
2791.readme-empty {
2792 margin: 0 0 20px;
2793 color: var(--fg-muted);
2794 font-size: calc(13.75px * var(--font-scale));
2795}
2796
2797/* Repositories beside the rail. */
2798.home-grid {
2799 display: grid;
2800 grid-template-columns: minmax(0, 1fr) 20rem;
2801 gap: 28px;
2802 align-items: start;
2803}
2804.home-main { min-width: 0; }
2805.home-rail { position: sticky; top: 16px; }
2806
2807.section-head {
2808 display: flex;
2809 align-items: baseline;
2810 justify-content: space-between;
2811 gap: 12px;
2812 margin-bottom: 9px;
2813}
2814.section-head h2 { margin: 0; }
2815.section-head .more { font-size: calc(12.65px * var(--font-scale)); color: var(--fg-muted); }
2816.section-head .more:hover { color: var(--accent); }
2817.section-head .muted { font-size: calc(12.65px * var(--font-scale)); }
2818
2819.repo-grid {
2820 display: grid;
2821 grid-template-columns: repeat(auto-fill, minmax(23rem, 1fr));
2822 gap: 12px;
2823}
2824.repo-card {
2825 display: flex;
2826 flex-direction: column;
2827 gap: 6px;
2828 min-width: 0;
2829 padding: 14px 16px;
2830 background: var(--bg-panel);
2831 border: 1px solid var(--border);
2832 border-radius: var(--radius-lg);
2833}
2834.repo-card:hover { border-color: var(--border-strong); background: var(--bg-raised); }
2835.repo-card-top { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
2836.repo-card h3 { font-size: calc(14.85px * var(--font-scale)); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2837.repo-card h3 a { color: var(--fg); font-weight: 600; }
2838.repo-card h3 a:hover { color: var(--accent); text-decoration: none; }
2839.repo-vis { display: inline-flex; flex: 0 0 auto; color: var(--fg-faint); }
2840.repo-vis .icon { width: 12px; height: 12px; }
2841.repo-lang { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; font-size: calc(11.55px * var(--font-scale)); color: var(--fg-faint); }
2842.lang-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--fg-faint); }
2843.repo-updated { margin-left: auto; flex: 0 0 auto; color: var(--fg-faint); font-size: calc(12.1px * var(--font-scale)); }
2844.repo-desc {
2845 color: var(--fg-muted);
2846 font-size: calc(13.2px * var(--font-scale));
2847 margin: 0;
2848 overflow: hidden;
2849 display: -webkit-box;
2850 -webkit-line-clamp: 2;
2851 -webkit-box-orient: vertical;
2852}
2853.repo-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 3px; }
2854.repo-chips .chip { padding: 1px 7px; font-size: calc(11.55px * var(--font-scale)); gap: 4px; }
2855.repo-chips .chip .icon { width: 11px; height: 11px; }
2856.repo-chips .status { font-size: calc(10.45px * var(--font-scale)); letter-spacing: 0.03em; }
2857.repo-chips .status::before { width: 5px; height: 5px; }
2858
2859.empty-state.first-run {
2860 display: flex;
2861 flex-direction: column;
2862 align-items: center;
2863 text-align: center;
2864 padding: 44px 20px;
2865}
2866.first-run .empty-icon {
2867 display: grid;
2868 place-items: center;
2869 width: 44px;
2870 height: 44px;
2871 margin-bottom: 12px;
2872 border-radius: 50%;
2873 background: var(--bg-raised);
2874 color: var(--fg-faint);
2875}
2876.first-run .empty-icon .icon { width: 20px; height: 20px; }
2877.first-run h2 { margin: 0 0 6px; font-size: calc(17.6px * var(--font-scale)); color: var(--fg); }
2878.first-run p { margin: 0 0 14px; }
2879.first-run .empty-actions { margin: 0 0 10px; }
2880.first-run .empty-hint { margin: 0; font-size: calc(12.65px * var(--font-scale)); color: var(--fg-faint); }
2881.first-run .empty-hint code { font-size: calc(12.1px * var(--font-scale)); }
2882
2883@media (max-width: 1040px) {
2884 .home-grid { grid-template-columns: 1fr; }
2885 .home-rail { position: static; order: 2; }
2886 .home-main { order: 1; }
2887}
2888@media (max-width: 720px) {
2889 .hero { flex-wrap: wrap; align-items: flex-start; padding-top: 18px; }
2890 .hero-actions { width: 100%; }
2891 .hero h1 { font-size: calc(24.2px * var(--font-scale)); }
2892 .repo-grid { grid-template-columns: 1fr; }
2893}
2894
2895/* ---------- status page ---------- */
2896
2897/* No rule under the header itself; the tab row keeps its rule. */
2898.status-head { border-bottom: 0; margin-bottom: 8px; }
2899.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 10px; margin: 14px 0; }
2900.status-grid .card { margin: 0; display: flex; flex-direction: column; gap: 4px; }
2901.status-grid strong { font-size: calc(22px * var(--font-scale)); font-weight: 600; font-variant-numeric: tabular-nums; }
2902.status-note { color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); }
2903.status-total td { font-weight: 600; border-top: 1px solid var(--border-strong); }
2904/* Audit detail is attacker-influenced text: wrap it instead of widening the page. */
2905.audit-detail { overflow-wrap: anywhere; }
2906.check-state { font-size: calc(12.1px * var(--font-scale)); text-transform: uppercase; letter-spacing: 0.04em; }
2907.check-state.ok { color: var(--good); }
2908.check-state.warn { color: var(--warn); }
2909.check-state.fail { color: var(--bad); }
2910
2911/* The admin-only effective-configuration table below the health checks. */
2912.config-table td:first-child { width: 15rem; }
2913.config-table td:first-child code { white-space: nowrap; }
2914.config-table td:last-child { overflow-wrap: anywhere; }
2915.config-group td {
2916 background: var(--bg-raised);
2917 color: var(--fg-muted);
2918 font-size: calc(12.1px * var(--font-scale));
2919 font-weight: 500;
2920 letter-spacing: 0.04em;
2921 text-transform: uppercase;
2922 padding-top: 7px;
2923 padding-bottom: 7px;
2924}
2925.config-group:hover td { background: var(--bg-raised); }
2926
2927/* ---------- language colours ---------- */
2928
2929/* Language hints for file rows. Active tree rows still win through specificity. */
2930.tree-icon.lang-go, .dirlist a.file.lang-go .icon { color: #00add8; }
2931.tree-icon.lang-rust, .dirlist a.file.lang-rust .icon { color: #dea584; }
2932.tree-icon.lang-js, .dirlist a.file.lang-js .icon { color: #f1e05a; }
2933.tree-icon.lang-ts, .dirlist a.file.lang-ts .icon { color: #3178c6; }
2934.tree-icon.lang-py, .dirlist a.file.lang-py .icon { color: #3572a5; }
2935.tree-icon.lang-rb, .dirlist a.file.lang-rb .icon { color: #cc342d; }
2936.tree-icon.lang-jvm, .dirlist a.file.lang-jvm .icon { color: #b07219; }
2937.tree-icon.lang-c, .dirlist a.file.lang-c .icon { color: #a8b9cc; }
2938.tree-icon.lang-cpp, .dirlist a.file.lang-cpp .icon { color: #f34b7d; }
2939.tree-icon.lang-cs, .dirlist a.file.lang-cs .icon { color: #178600; }
2940.tree-icon.lang-php, .dirlist a.file.lang-php .icon { color: #8f9ad8; }
2941.tree-icon.lang-sh, .dirlist a.file.lang-sh .icon { color: #89e051; }
2942.tree-icon.lang-html, .dirlist a.file.lang-html .icon { color: #e34c26; }
2943.tree-icon.lang-css, .dirlist a.file.lang-css .icon { color: #a074c4; }
2944.tree-icon.lang-config, .dirlist a.file.lang-config .icon { color: #cbcb41; }
2945.tree-icon.lang-text, .dirlist a.file.lang-text .icon { color: #519aba; }
2946.tree-icon.lang-sql, .dirlist a.file.lang-sql .icon { color: #e38c00; }
2947.tree-icon.lang-image, .dirlist a.file.lang-image .icon { color: #a074c4; }
2948.tree-icon.lang-lock, .dirlist a.file.lang-lock .icon { color: var(--fg-muted); }
2949.tree-icon.lang-build, .dirlist a.file.lang-build .icon { color: var(--fg-muted); }
2950
2951/* The same palette marks a repository's top language on the landing page. */
2952.lang-dot.lang-go { background: #00add8; }
2953.lang-dot.lang-rust { background: #dea584; }
2954.lang-dot.lang-js { background: #f1e05a; }
2955.lang-dot.lang-ts { background: #3178c6; }
2956.lang-dot.lang-py { background: #3572a5; }
2957.lang-dot.lang-rb { background: #cc342d; }
2958.lang-dot.lang-jvm { background: #b07219; }
2959.lang-dot.lang-c { background: #a8b9cc; }
2960.lang-dot.lang-cpp { background: #f34b7d; }
2961.lang-dot.lang-cs { background: #178600; }
2962.lang-dot.lang-php { background: #8f9ad8; }
2963.lang-dot.lang-sh { background: #89e051; }
2964.lang-dot.lang-html { background: #e34c26; }
2965.lang-dot.lang-css { background: #a074c4; }
2966.lang-dot.lang-config { background: #cbcb41; }
2967.lang-dot.lang-text { background: #519aba; }
2968.lang-dot.lang-sql { background: #e38c00; }
2969.lang-dot.lang-image { background: #a074c4; }
2970.lang-dot.lang-lock, .lang-dot.lang-build { background: var(--fg-muted); }
2971
2972/* ---------- profiles and the account directory ---------- */
2973
2974.profile-joined { margin: 4px 0 0; font-size: calc(12.65px * var(--font-scale)); }
2975.profile-scope { margin: 8px 0 0; font-size: calc(12.65px * var(--font-scale)); }
2976
2977
2978/* The four numbers a profile leads with. Each one is a link to the feed it
2979 counts, so a number is a door rather than a dead end. */
2980.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 10px; margin: 18px 0 0; }
2981.stat {
2982 display: flex;
2983 flex-direction: column;
2984 gap: 3px;
2985 padding: 14px 16px;
2986 color: var(--fg);
2987 background: var(--bg-panel);
2988 border: 1px solid var(--border);
2989 border-radius: var(--radius-lg);
2990}
2991.stat:hover { color: var(--fg); background: var(--bg-raised); border-color: var(--border-strong); text-decoration: none; }
2992.stat-num { font-size: calc(24px * var(--font-scale)); font-weight: 600; font-variant-numeric: tabular-nums; }
2993.stat-label { color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); }
2994
2995.profile-repos, .profile-activity { margin-top: 26px; }
2996.profile-repo { gap: 4px; }
2997.profile-repo-meta { display: flex; flex-wrap: wrap; gap: 4px 10px; margin: 0; color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); }
2998
2999/* The user directory is a table: one row per account, sortable headers, and
3000 counts right-aligned so a column can be scanned. */
3001.people-name { font-weight: 600; }
3002.user-scope { margin: 0 0 10px; font-size: calc(12.65px * var(--font-scale)); }
3003.user-list th a { color: inherit; }
3004.user-list th a:hover { color: var(--accent); text-decoration: none; }
3005.user-list .sort-mark { margin-left: 4px; color: var(--accent); }
3006.user-list th.count,
3007.user-list td.count { text-align: right; font-variant-numeric: tabular-nums; }
3008.user-list td.count { color: var(--fg-muted); }
3009.user-list .state { margin-left: 4px; vertical-align: 1px; }
3010
3011@media (max-width: 720px) {
3012 .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
3013}
3014