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.

Build out the forge web UI

f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264

Build out the forge web UI

Turn the web surface into a compact, server-rendered forge with HTMX as the
progressive dynamic layer.

- Split templates and assets into embedded files; add a tokenised dark theme,
  Lucide icons, and vendored HTMX.
- Add the repository Overview page and a separate chronological Activity feed
  with type filters and HTMX fragment updates.
- Finish the issue and change workflows: create pages, comments, close/reopen,
  labels and assignees, approval, discussion, line-anchored review comments,
  and HTMX list filters.
- Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware
  code browsing, and a repository Runs list.
- Add a loopback-only development login for local write testing, with explicit
  configuration and listener checks.
- Extend tests for the new pages, fragments, labels, assignees, anchors, and
  development login.

server/web/static/vole.css +1143

@@ -0,0 +1,1143 @@
1 /* Vole's web surface. One stylesheet, token-driven: a light theme is another
2 block of variables, and a real identity is a change to the top block only. */
3
4 :root {
5 color-scheme: dark;
6
7 /* surfaces */
8 --bg: #0e0f11;
9 --bg-panel: #141517;
10 --bg-raised: #191a1e;
11 --bg-hover: #1f2024;
12 --bg-active: #24262b;
13 --border: #26282d;
14 --border-strong: #35383f;
15
16 /* text */
17 --fg: #e7e8ea;
18 --fg-muted: #8b909a;
19 --fg-faint: #696e78;
20
21 /* accent and status */
22 --accent: #7c86ff;
23 --accent-fg: #ffffff;
24 --accent-soft: rgba(124, 134, 255, 0.14);
25 --good: #4cb782;
26 --good-soft: rgba(76, 183, 130, 0.12);
27 --bad: #eb5757;
28 --bad-soft: rgba(235, 87, 87, 0.12);
29 --warn: #f2c94c;
30
31 /* shape and type */
32 --radius: 6px;
33 --radius-lg: 8px;
34 --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
35 --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
36 --fs: 13px;
37 --lh: 1.5;
38 }
39
40 * { box-sizing: border-box; }
41 * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
42 ::-webkit-scrollbar { width: 10px; height: 10px; }
43 ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; }
44 ::-webkit-scrollbar-track { background: transparent; }
45
46 html, body { height: 100%; }
47
48 body {
49 margin: 0;
50 background: var(--bg);
51 color: var(--fg);
52 font: var(--fs)/var(--lh) var(--font);
53 -webkit-font-smoothing: antialiased;
54 }
55
56 a { color: var(--accent); text-decoration: none; }
57 a:hover { text-decoration: underline; }
58
59 h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.005em; }
60 h1 { font-size: 18px; line-height: 1.35; margin: 2px 0 12px; }
61 h2 { font-size: 13px; color: var(--fg-muted); margin: 24px 0 8px; }
62 h3 { font-size: 13px; }
63 p { margin: 0 0 10px; }
64 ul, ol { margin: 0 0 10px; padding-left: 20px; }
65 hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
66 img { max-width: 100%; }
67
68 .muted { color: var(--fg-muted); }
69 .sha { font-family: var(--mono); font-size: 12px; color: var(--fg-muted); }
70 .nowrap { white-space: nowrap; }
71 .adds { color: var(--good); }
72 .dels { color: var(--bad); }
73 .conflict, .error { color: var(--bad); }
74 .bad { color: var(--bad); }
75
76 /* ---------- chrome ---------- */
77
78 .topbar {
79 display: flex;
80 align-items: center;
81 gap: 20px;
82 height: 46px;
83 padding: 0 20px;
84 background: color-mix(in srgb, var(--bg-panel) 92%, transparent);
85 border-bottom: 1px solid var(--border);
86 position: sticky;
87 top: 0;
88 z-index: 20;
89 backdrop-filter: blur(8px);
90 }
91
92 .brand {
93 display: inline-flex;
94 align-items: center;
95 gap: 8px;
96 color: var(--fg);
97 font-weight: 650;
98 letter-spacing: -0.01em;
99 }
100 .brand:hover { text-decoration: none; }
101 .brand .mark {
102 display: inline-grid;
103 place-items: center;
104 width: 20px;
105 height: 20px;
106 border-radius: 5px;
107 background: var(--accent);
108 color: var(--accent-fg);
109 font: 700 12px/1 var(--mono);
110 }
111
112 .topnav { display: flex; gap: 16px; }
113 .topnav a { color: var(--fg-muted); font-size: 12.5px; }
114 .topnav a:hover { color: var(--fg); text-decoration: none; }
115
116 .topright {
117 margin-left: auto;
118 display: flex;
119 align-items: center;
120 gap: 12px;
121 font-size: 12.5px;
122 color: var(--fg-muted);
123 }
124 .topright .account { color: var(--fg-muted); }
125 .topright .account:hover { color: var(--fg); text-decoration: none; }
126
127 .page {
128 width: min(100%, 1440px);
129 margin: 0 auto;
130 padding: 20px 24px 72px;
131 }
132
133 @media (max-width: 720px) {
134 .tabs { mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent); }
135 .topbar { padding: 0 12px; gap: 12px; }
136 .page { padding: 16px 12px 56px; }
137 .topnav { gap: 10px; }
138 }
139
140 /* ---------- repository header ---------- */
141
142 .repohead {
143 display: flex;
144 flex-wrap: wrap;
145 align-items: baseline;
146 gap: 6px 16px;
147 padding-bottom: 0;
148 margin-bottom: 16px;
149 border-bottom: 1px solid var(--border);
150 }
151
152 .reponame {
153 color: var(--fg);
154 font-size: 16px;
155 font-weight: 650;
156 letter-spacing: -0.01em;
157 padding: 0 2px 10px;
158 }
159 .reponame:hover { text-decoration: none; color: var(--accent); }
160
161 .tabs {
162 display: flex;
163 gap: 2px;
164 overflow-x: auto;
165 scrollbar-width: none;
166 margin-left: -8px;
167 }
168 .tabs::-webkit-scrollbar { display: none; }
169 .tabs a {
170 color: var(--fg-muted);
171 padding: 8px 10px 9px;
172 border-bottom: 2px solid transparent;
173 white-space: nowrap;
174 font-size: 12.5px;
175 }
176 .tabs a:hover { color: var(--fg); text-decoration: none; }
177 .tabs a.active { color: var(--fg); border-bottom-color: var(--accent); }
178
179 /* ---------- panels and tables ---------- */
180
181 .panel {
182 border: 1px solid var(--border);
183 border-radius: var(--radius-lg);
184 overflow: hidden;
185 margin: 12px 0;
186 background: var(--bg-panel);
187 }
188
189 .card {
190 background: var(--bg-panel);
191 border: 1px solid var(--border);
192 border-radius: var(--radius-lg);
193 padding: 14px;
194 margin: 12px 0;
195 }
196
197 table {
198 width: 100%;
199 border-collapse: collapse;
200 font-size: 12.5px;
201 }
202 .panel > table { margin: 0; }
203 th, td {
204 text-align: left;
205 padding: 8px 12px;
206 border-bottom: 1px solid var(--border);
207 vertical-align: top;
208 }
209 tr:last-child > td { border-bottom: 0; }
210 th {
211 color: var(--fg-faint);
212 font-weight: 500;
213 font-size: 11px;
214 text-transform: uppercase;
215 letter-spacing: 0.04em;
216 background: var(--bg-raised);
217 padding-top: 7px;
218 padding-bottom: 7px;
219 }
220 tbody tr:hover { background: var(--bg-hover); }
221
222 table.commits td { padding-top: 7px; padding-bottom: 7px; }
223 table.commits td:first-child { width: 9ch; }
224
225 /* ---------- controls ---------- */
226
227 button, .button {
228 display: inline-flex;
229 align-items: center;
230 justify-content: center;
231 gap: 6px;
232 background: var(--accent);
233 color: var(--accent-fg);
234 border: 1px solid transparent;
235 border-radius: var(--radius);
236 padding: 5px 11px;
237 font: 500 12.5px/1.4 var(--font);
238 cursor: pointer;
239 white-space: nowrap;
240 }
241 button:hover, .button:hover { filter: brightness(1.08); text-decoration: none; }
242 button:disabled { opacity: 0.5; cursor: default; }
243 .button.secondary, button.secondary {
244 background: var(--bg-raised);
245 color: var(--fg);
246 border-color: var(--border-strong);
247 }
248 .button.secondary:hover { background: var(--bg-hover); }
249
250 button.link, .link {
251 background: none;
252 border: 0;
253 color: var(--accent);
254 padding: 0;
255 font: inherit;
256 font-weight: 400;
257 }
258 button.link:hover, .link:hover { text-decoration: underline; }
259 form.inline { display: inline; }
260
261 select, input[type="text"], input[type="password"], input[type="search"], textarea {
262 background: var(--bg);
263 color: var(--fg);
264 border: 1px solid var(--border-strong);
265 border-radius: var(--radius);
266 padding: 5px 8px;
267 font: inherit;
268 font-size: 12.5px;
269 }
270 select:focus, input:focus, textarea:focus {
271 outline: 2px solid var(--accent-soft);
272 outline-offset: 1px;
273 border-color: var(--accent);
274 }
275 input[type="text"], input[type="password"], input[type="search"], textarea {
276 width: 100%;
277 max-width: 34rem;
278 }
279 textarea { min-height: 7rem; resize: vertical; line-height: 1.5; }
280 label { display: block; margin: 10px 0; color: var(--fg-muted); font-size: 12.5px; }
281 /* The label text sits above the control. Without this, a max-width input shares
282 the line with its label and the form reads as a row of pairs. */
283 label > input, label > textarea, label > select {
284 display: block;
285 margin-top: 4px;
286 color: var(--fg);
287 }
288 label > select { width: 100%; max-width: 34rem; }
289
290 .filters {
291 display: flex;
292 flex-wrap: wrap;
293 align-items: center;
294 gap: 8px;
295 margin: 12px 0;
296 }
297 .filters input[type="text"] { max-width: 18rem; }
298
299 /* ---------- labels, flashes, empty pages ---------- */
300
301 .label {
302 display: inline-block;
303 margin-left: 6px;
304 padding: 1px 6px;
305 border: 1px solid var(--border-strong);
306 border-radius: 10px;
307 font-size: 11px;
308 line-height: 1.5;
309 color: var(--fg-muted);
310 vertical-align: 1px;
311 }
312 a.label:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
313
314 .flash {
315 border: 1px solid var(--border-strong);
316 background: var(--bg-panel);
317 border-radius: var(--radius);
318 padding: 8px 12px;
319 margin-bottom: 14px;
320 font-size: 12.5px;
321 }
322 .flash.bad { border-color: color-mix(in srgb, var(--bad) 50%, var(--border)); background: var(--bad-soft); }
323
324 .empty-page {
325 max-width: 34rem;
326 margin: 12vh auto 0;
327 text-align: center;
328 }
329 .empty-page .eyebrow {
330 color: var(--fg-faint);
331 font: 600 11px/1 var(--mono);
332 letter-spacing: 0.1em;
333 text-transform: uppercase;
334 margin-bottom: 10px;
335 }
336 .empty-page h1 { font-size: 24px; margin-bottom: 8px; }
337 .empty-page p { margin-bottom: 18px; }
338
339 /* ---------- auth ---------- */
340
341 .auth { max-width: 34rem; margin-top: 8vh; }
342 .auth h1 { margin-bottom: 6px; }
343
344 /* ---------- repo home ---------- */
345
346 .repo-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin: 0 0 14px; }
347 .repo-sub .item { color: var(--fg-muted); font-size: 12.5px; }
348
349 details.clone { position: relative; }
350 details.clone > summary {
351 list-style: none;
352 cursor: pointer;
353 display: inline-flex;
354 align-items: center;
355 gap: 6px;
356 background: var(--bg-raised);
357 color: var(--fg);
358 border: 1px solid var(--border-strong);
359 border-radius: var(--radius);
360 padding: 5px 11px;
361 font-size: 12.5px;
362 }
363 details.clone > summary::-webkit-details-marker { display: none; }
364 details.clone > summary:hover { background: var(--bg-hover); }
365 details.clone[open] > summary { border-color: var(--accent); }
366 .clone-pop {
367 position: absolute;
368 z-index: 10;
369 top: calc(100% + 6px);
370 left: 0;
371 width: min(32rem, 80vw);
372 background: var(--bg-raised);
373 border: 1px solid var(--border-strong);
374 border-radius: var(--radius-lg);
375 padding: 12px;
376 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
377 }
378 .clone-pop p { margin: 8px 0 2px; }
379 .clone-pop code { display: block; }
380
381 .row { display: flex; gap: 16px; flex-wrap: wrap; }
382 .row > div { flex: 1 1 18rem; min-width: 0; }
383
384 .readme { margin: 16px 0; }
385
386 /* ---------- code, diffs, markdown ---------- */
387
388 code, pre, kbd {
389 font-family: var(--mono);
390 }
391 code {
392 background: var(--bg-raised);
393 border: 1px solid var(--border);
394 border-radius: 4px;
395 padding: 1px 5px;
396 font-size: 11.5px;
397 }
398 pre {
399 background: var(--bg-panel);
400 border: 1px solid var(--border);
401 border-radius: var(--radius);
402 padding: 12px 14px;
403 overflow: auto;
404 font-size: 12px;
405 line-height: 1.55;
406 margin: 10px 0;
407 }
408 pre code { background: none; border: 0; padding: 0; font-size: inherit; }
409
410 .crumbs { color: var(--fg-muted); font-size: 12.5px; margin: -4px 0 12px; }
411 .crumbs a { color: var(--fg-muted); }
412 .crumbs a:hover { color: var(--accent); }
413 .dir { color: var(--accent); }
414
415 .md { line-height: 1.65; max-width: 90ch; }
416 .md > :first-child { margin-top: 0; }
417 .md > :last-child { margin-bottom: 0; }
418 .md h1, .md h2, .md h3, .md h4 {
419 color: var(--fg);
420 margin: 20px 0 8px;
421 letter-spacing: -0.01em;
422 text-transform: none;
423 }
424 .md h1 { font-size: 20px; }
425 .md h2 { font-size: 16px; }
426 .md h3 { font-size: 14px; }
427 .md h4 { font-size: 13px; }
428 .md p { margin: 9px 0; }
429 .md ul, .md ol { padding-left: 22px; margin: 9px 0; }
430 .md blockquote {
431 margin: 10px 0;
432 padding: 2px 0 2px 12px;
433 border-left: 3px solid var(--border-strong);
434 color: var(--fg-muted);
435 }
436 .md pre { background: var(--bg-panel); }
437 .md table { width: auto; }
438 .md img { border-radius: var(--radius); }
439
440 .diffsum { font-size: 12px; margin: 10px 0; }
441
442 .diff {
443 border: 1px solid var(--border);
444 border-radius: var(--radius-lg);
445 margin: 12px 0;
446 overflow: hidden;
447 background: var(--bg-panel);
448 }
449 .diff h3 {
450 margin: 0;
451 padding: 8px 12px;
452 background: var(--bg-raised);
453 border-bottom: 1px solid var(--border);
454 font-size: 12px;
455 font-weight: 550;
456 }
457 .diff h3 .path { font-family: var(--mono); }
458 .diff table {
459 font-family: var(--mono);
460 font-size: 11.5px;
461 border-collapse: collapse;
462 }
463 .diff td {
464 border: 0;
465 padding: 1px 10px;
466 white-space: pre-wrap;
467 word-break: break-word;
468 vertical-align: top;
469 line-height: 19px;
470 }
471 .diff tbody tr:hover { background: none; }
472 .diff td.ln {
473 width: 3.5rem;
474 text-align: right;
475 color: var(--fg-faint);
476 user-select: none;
477 border-right: 1px solid var(--border);
478 padding: 1px 8px;
479 }
480 .diff td.code { padding-left: 12px; }
481 .diff tr.add { background: var(--good-soft); }
482 .diff tr.add td.ln { color: var(--good); }
483 .diff tr.del { background: var(--bad-soft); }
484 .diff tr.del td.ln { color: var(--bad); }
485 .diff tr.hunk td {
486 color: var(--fg-muted);
487 background: var(--bg-raised);
488 padding: 5px 12px;
489 font-size: 11px;
490 border-top: 1px solid var(--border);
491 border-bottom: 1px solid var(--border);
492 }
493 .diff tr.note td {
494 background: var(--accent-soft);
495 padding: 3px 12px 4px;
496 font-size: 11px;
497 }
498 .diff tr.note a.label { color: var(--accent); border-color: var(--accent); }
499 .diff .binary { padding: 10px 12px; color: var(--fg-muted); font-size: 12px; margin: 0; }
500
501 /* ---------- responsive ---------- */
502
503 @media (max-width: 720px) {
504 h1 { font-size: 16px; }
505 .repohead { gap: 0 10px; }
506 .reponame { font-size: 15px; }
507 th, td { padding: 7px 9px; }
508 .nowrap { white-space: normal; }
509 }
510
511 /* ---------- overview ---------- */
512
513 .overview {
514 display: grid;
515 grid-template-columns: minmax(0, 1fr) 340px;
516 gap: 28px;
517 align-items: start;
518 }
519 .overview-main { min-width: 0; }
520 .overview-side section { padding: 12px 0 14px; border-bottom: 1px solid var(--border); }
521 .overview-side section:first-child { padding-top: 0; }
522 .overview-side section:last-child { border-bottom: 0; }
523 .overview-side h2 { margin: 0 0 8px; }
524
525 .with-more { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
526 .with-more .more { font-size: 12px; font-weight: 400; color: var(--fg-muted); }
527 .with-more .more:hover { color: var(--accent); }
528
529 .side-list { display: flex; flex-direction: column; gap: 6px; }
530 .side-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12.5px; }
531 .side-row .muted { font-size: 11.5px; white-space: nowrap; }
532
533 .contributors { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
534 .contributors li { display: flex; align-items: center; gap: 8px; min-width: 0; }
535 .contributors .cname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; }
536 .contributors .muted { font-size: 11.5px; }
537
538 .avatar {
539 display: inline-grid;
540 place-items: center;
541 flex: 0 0 auto;
542 width: 20px;
543 height: 20px;
544 border-radius: 50%;
545 font: 600 10px/1 var(--font);
546 color: #fff;
547 background: #4b5563;
548 }
549 .avatar.c1 { background: #5b67d8; }
550 .avatar.c2 { background: #2f7d6d; }
551 .avatar.c3 { background: #9a5b3c; }
552 .avatar.c4 { background: #8a4b78; }
553 .avatar.c5 { background: #3c6e9a; }
554 .avatar.c6 { background: #7a6a2f; }
555 .avatar.c7 { background: #5f5b6e; }
556
557 .lang { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; margin-bottom: 8px; font-size: 12px; }
558 .lang .muted { font-size: 11.5px; }
559 .bar {
560 grid-column: 1 / -1;
561 appearance: none;
562 width: 100%;
563 height: 5px;
564 border: 0;
565 border-radius: 3px;
566 background: var(--bg-active);
567 overflow: hidden;
568 }
569 .bar::-webkit-progress-bar { background: var(--bg-active); border-radius: 3px; }
570 .bar::-webkit-progress-value { background: var(--accent); border-radius: 3px; }
571 .bar::-moz-progress-bar { background: var(--accent); border-radius: 3px; }
572
573 .filehead {
574 display: flex;
575 align-items: center;
576 justify-content: space-between;
577 gap: 12px;
578 padding: 7px 12px;
579 background: var(--bg-raised);
580 border: 1px solid var(--border);
581 border-bottom: 0;
582 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
583 font-family: var(--mono);
584 font-size: 12px;
585 }
586 .filehead a { font-family: var(--font); font-size: 11.5px; color: var(--fg-muted); }
587 .filehead a:hover { color: var(--accent); }
588 .readme {
589 background: var(--bg-panel);
590 border: 1px solid var(--border);
591 border-radius: 0 0 var(--radius-lg) var(--radius-lg);
592 padding: 18px 20px;
593 margin: 0 0 20px;
594 max-width: none;
595 }
596 .readme > :first-child { margin-top: 0; }
597
598 .latest {
599 display: flex;
600 flex-wrap: wrap;
601 align-items: baseline;
602 gap: 4px 10px;
603 padding: 7px 12px;
604 margin: 0 0 16px;
605 background: var(--bg-panel);
606 border: 1px solid var(--border);
607 border-radius: var(--radius);
608 font-size: 12.5px;
609 }
610 .latest .muted { font-size: 11.5px; }
611 .latest a.sha { margin-left: auto; }
612
613 .status {
614 display: inline-flex;
615 align-items: center;
616 gap: 5px;
617 font-size: 11px;
618 font-weight: 500;
619 line-height: 1.4;
620 text-transform: uppercase;
621 letter-spacing: 0.04em;
622 white-space: nowrap;
623 color: var(--fg-muted);
624 }
625 .status::before {
626 content: "";
627 width: 6px;
628 height: 6px;
629 border-radius: 50%;
630 background: currentColor;
631 }
632 .status.success { color: var(--good); }
633 .status.failed { color: var(--bad); }
634 .status.running { color: var(--accent); }
635 .status.queued { color: var(--warn); }
636 .status.canceled { color: var(--fg-faint); }
637
638 .chip {
639 display: inline-flex;
640 align-items: center;
641 padding: 3px 9px;
642 border: 1px solid var(--border-strong);
643 border-radius: 12px;
644 font-size: 11.5px;
645 color: var(--fg-muted);
646 background: var(--bg-panel);
647 }
648 .chip:hover { color: var(--fg); border-color: var(--fg-faint); text-decoration: none; }
649 .chip.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
650
651 td.fit { width: 1%; white-space: nowrap; }
652
653 @media (max-width: 1040px) {
654 .overview { grid-template-columns: 1fr; }
655 .overview-side { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 24px; }
656 .overview-side section:first-child { padding-top: 12px; }
657 }
658
659 /* ---------- code browsing ---------- */
660
661 .path-title {
662 font-family: var(--mono);
663 font-size: 16px;
664 font-weight: 550;
665 word-break: break-all;
666 margin-bottom: 4px;
667 }
668 .crumb-actions { float: right; display: inline-flex; align-items: baseline; gap: 10px; font-family: var(--font); }
669 .crumb-actions .muted { font-size: 11.5px; }
670
671 .readme > *,
672 .filebody > * { max-width: 84ch; }
673 .readme > pre,
674 .readme > table,
675 .filebody > pre,
676 .filebody > table { max-width: 100%; }
677
678 .filebody {
679 background: var(--bg-panel);
680 border: 1px solid var(--border);
681 border-radius: var(--radius-lg);
682 padding: 18px 20px;
683 margin: 12px 0 20px;
684 }
685 .filebody > :first-child { margin-top: 0; }
686
687 .blob {
688 max-height: 76vh;
689 overflow: auto;
690 }
691 .blob table.code {
692 font-family: var(--mono);
693 font-size: 11.5px;
694 border-collapse: collapse;
695 }
696 .blob td {
697 border: 0;
698 padding: 0 12px;
699 line-height: 19px;
700 white-space: pre;
701 vertical-align: top;
702 }
703 .blob td.ln {
704 width: 3.5rem;
705 text-align: right;
706 color: var(--fg-faint);
707 user-select: none;
708 border-right: 1px solid var(--border);
709 padding: 0 8px;
710 }
711 .blob td.code { padding-left: 12px; }
712 .blob tr:hover { background: none; }
713
714 /* ---------- syntax highlighting ---------- */
715
716 .highlight {
717 border: 1px solid var(--border);
718 border-radius: var(--radius-lg);
719 background: var(--bg-panel);
720 overflow: auto;
721 max-height: 76vh;
722 margin: 12px 0 20px;
723 }
724 .highlight pre.chroma {
725 margin: 0;
726 padding: 12px 0;
727 border: 0;
728 background: transparent;
729 font-family: var(--mono);
730 font-size: 12px;
731 line-height: 19px;
732 overflow: visible;
733 }
734 .chroma .line { display: block; }
735 .chroma .ln {
736 display: inline-block;
737 width: 3.5rem;
738 padding-right: 8px;
739 margin-right: 12px;
740 text-align: right;
741 color: var(--fg-faint);
742 user-select: none;
743 border-right: 1px solid var(--border);
744 }
745 .chroma .cl { white-space: pre; }
746 .chroma .w { color: inherit; }
747
748 .chroma .c, .chroma .ch, .chroma .cm, .chroma .c1, .chroma .cs,
749 .chroma .cp, .chroma .cpf { color: #6f7682; font-style: italic; }
750 .chroma .k, .chroma .kc, .chroma .kd, .chroma .kn, .chroma .kp,
751 .chroma .kr, .chroma .kt { color: #c792ea; }
752 .chroma .s, .chroma .sa, .chroma .sb, .chroma .sc, .chroma .dl,
753 .chroma .sd, .chroma .s2, .chroma .se, .chroma .sh, .chroma .si,
754 .chroma .sx, .chroma .sr, .chroma .s1, .chroma .ss { color: #a8d98a; }
755 .chroma .m, .chroma .mb, .chroma .mf, .chroma .mh, .chroma .mi,
756 .chroma .il, .chroma .mo { color: #f2a06b; }
757 .chroma .nf, .chroma .fm { color: #83aaff; }
758 .chroma .nc, .chroma .nn, .chroma .ne, .chroma .nd { color: #f0c674; }
759 .chroma .nb, .chroma .bp { color: #7fd3e8; }
760 .chroma .o, .chroma .ow { color: #7fd3e8; }
761 .chroma .p, .chroma .pi { color: #a6accd; }
762 .chroma .nt { color: #c792ea; }
763 .chroma .na { color: #f0c674; }
764 .chroma .nv, .chroma .vc, .chroma .vg, .chroma .vi, .chroma .vm { color: #e7e8ea; }
765 .chroma .gd { color: var(--bad); }
766 .chroma .gi { color: var(--good); }
767 .chroma .err { color: var(--bad); text-decoration: underline; }
768
769 /* Compact activity rows in the overview sidebar. */
770
771 .side-commit,
772 .side-run {
773 display: flex;
774 flex-direction: column;
775 gap: 1px;
776 padding: 4px 0;
777 color: var(--fg);
778 min-width: 0;
779 }
780 .side-commit:hover,
781 .side-run:hover { color: var(--accent); text-decoration: none; }
782 .side-subject {
783 font-size: 12.5px;
784 overflow: hidden;
785 text-overflow: ellipsis;
786 white-space: nowrap;
787 }
788 .side-meta {
789 display: flex;
790 gap: 8px;
791 font-size: 11px;
792 color: var(--fg-faint);
793 white-space: nowrap;
794 }
795 .side-run-top { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
796 .side-run-top .status { flex: 0 0 auto; }
797 .side-size { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 2px; }
798
799 /* ---------- issues and changes ---------- */
800
801 .page-head {
802 display: flex;
803 align-items: flex-start;
804 justify-content: space-between;
805 gap: 16px;
806 margin-bottom: 12px;
807 }
808 .page-head h1 { margin: 0 0 3px; }
809 .page-sub { color: var(--fg-muted); font-size: 12.5px; margin: 0; }
810
811 .state {
812 display: inline-flex;
813 align-items: center;
814 gap: 5px;
815 padding: 1px 7px;
816 border: 1px solid var(--border-strong);
817 border-radius: 10px;
818 font-size: 10.5px;
819 line-height: 1.6;
820 font-weight: 500;
821 text-transform: uppercase;
822 letter-spacing: 0.04em;
823 color: var(--fg-muted);
824 white-space: nowrap;
825 }
826 .state::before {
827 content: "";
828 width: 6px;
829 height: 6px;
830 border-radius: 50%;
831 background: currentColor;
832 }
833 .state.open {
834 color: var(--good);
835 border-color: color-mix(in srgb, var(--good) 45%, var(--border));
836 background: var(--good-soft);
837 }
838 .state.closed { color: var(--fg-faint); }
839 .state.merged {
840 color: var(--accent);
841 border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
842 background: var(--accent-soft);
843 }
844
845 .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
846 .chip-count { margin-left: 5px; color: var(--fg-faint); font-variant-numeric: tabular-nums; }
847 .chip.active .chip-count { color: var(--accent); }
848 .filter-label {
849 color: var(--fg-faint);
850 font-size: 11px;
851 text-transform: uppercase;
852 letter-spacing: 0.04em;
853 }
854
855 .row-title { font-weight: 550; color: var(--fg); }
856 .row-title:hover { color: var(--accent); text-decoration: none; }
857 .row-meta {
858 display: flex;
859 flex-wrap: wrap;
860 align-items: center;
861 gap: 4px 8px;
862 margin-top: 2px;
863 font-size: 11.5px;
864 color: var(--fg-faint);
865 }
866 .row-meta .label { margin-left: 0; }
867
868 .empty-state {
869 border: 1px dashed var(--border-strong);
870 border-radius: var(--radius-lg);
871 padding: 20px;
872 margin: 14px 0;
873 color: var(--fg-muted);
874 }
875 .empty-state p { margin: 0 0 8px; }
876 .empty-state p:last-child { margin-bottom: 0; }
877 .empty-state pre { margin: 8px 0; }
878
879 /* Thread layout: conversation in the main column, action and metadata beside it. */
880 .thread {
881 display: grid;
882 grid-template-columns: minmax(0, 1fr) 280px;
883 gap: 24px;
884 align-items: start;
885 }
886 .thread-main { min-width: 0; }
887 .thread-side section { padding: 12px 0 14px; border-bottom: 1px solid var(--border); }
888 .thread-side section:first-child { padding-top: 0; }
889 .thread-side section:last-child { border-bottom: 0; }
890 .thread-side h2 { margin: 0 0 8px; }
891 .thread-side form { margin: 8px 0 0; }
892 .thread-side .meta-list { margin-bottom: 0; }
893
894 .comment {
895 background: var(--bg-panel);
896 border: 1px solid var(--border);
897 border-radius: var(--radius-lg);
898 margin: 10px 0;
899 overflow: hidden;
900 }
901 .comment-head {
902 display: flex;
903 flex-wrap: wrap;
904 align-items: center;
905 gap: 6px 8px;
906 padding: 7px 12px;
907 background: var(--bg-raised);
908 border-bottom: 1px solid var(--border);
909 font-size: 12px;
910 }
911 .comment-author { font-weight: 550; }
912 .comment-head .muted { font-size: 11.5px; }
913 .comment-body { padding: 12px 14px; }
914 .comment-body > :first-child { margin-top: 0; }
915 .comment-body > :last-child { margin-bottom: 0; }
916
917 .meta-list {
918 display: grid;
919 grid-template-columns: auto minmax(0, 1fr);
920 gap: 4px 10px;
921 margin: 0;
922 font-size: 12px;
923 }
924 .meta-list dt { color: var(--fg-faint); }
925 .meta-list dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
926 .label-row { display: inline-flex; flex-wrap: wrap; gap: 4px; }
927 .label-row .label { margin-left: 0; }
928
929 .approval-list, .check-list {
930 list-style: none;
931 padding: 0;
932 margin: 0;
933 display: flex;
934 flex-direction: column;
935 gap: 6px;
936 font-size: 12px;
937 }
938 .approval-list li, .check-list li { display: flex; align-items: center; gap: 7px; min-width: 0; }
939 .approval-list .cname, .check-list a {
940 flex: 1;
941 min-width: 0;
942 overflow: hidden;
943 text-overflow: ellipsis;
944 white-space: nowrap;
945 }
946 .approval-list .muted { font-size: 11px; }
947
948 @media (max-width: 900px) {
949 .thread { grid-template-columns: 1fr; }
950 .thread-side { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 24px; }
951 .thread-side section:first-child { padding-top: 12px; }
952 .page-head { flex-wrap: wrap; }
953 }
954
955 /* Form cards: fields live on a card, actions sit below it and align right.
956 Every primary action uses the same height and horizontal padding. */
957 .form-block { margin: 12px 0; }
958 .form-card { margin: 0; }
959 .form-actions {
960 display: flex;
961 justify-content: flex-end;
962 align-items: center;
963 gap: 8px;
964 margin-top: 8px;
965 }
966 .auth > .card { margin: 0; }
967 .auth > .form-actions { margin-top: 8px; }
968 .form-card input[type="text"],
969 .form-card input[type="password"],
970 .form-card input[type="search"],
971 .form-card textarea,
972 .form-card select {
973 width: 100%;
974 max-width: none;
975 }
976
977 button, .button {
978 min-height: 30px;
979 padding-top: 0;
980 padding-bottom: 0;
981 }
982 button.link, .link { min-height: 0; }
983
984 /* Label token input: the bordered box is the control, the text input inside it
985 adds labels, and selected labels are removable chips. */
986 .label-input { position: relative; }
987 .label-tokens {
988 display: flex;
989 flex-wrap: wrap;
990 align-items: center;
991 gap: 5px;
992 min-height: 32px;
993 padding: 3px 8px;
994 background: var(--bg);
995 border: 1px solid var(--border-strong);
996 border-radius: var(--radius);
997 }
998 .label-tokens:focus-within {
999 border-color: var(--accent);
1000 outline: 2px solid var(--accent-soft);
1001 outline-offset: 1px;
1002 }
1003 .label-token {
1004 display: inline-flex;
1005 align-items: center;
1006 gap: 3px;
1007 padding: 1px 3px 1px 8px;
1008 border-radius: 10px;
1009 background: var(--accent-soft);
1010 color: var(--accent);
1011 font-size: 11.5px;
1012 line-height: 1.6;
1013 }
1014 .label-remove {
1015 min-height: 0;
1016 padding: 0 3px;
1017 background: none;
1018 border: 0;
1019 color: inherit;
1020 font-size: 14px;
1021 line-height: 1;
1022 cursor: pointer;
1023 }
1024 .label-remove:hover { color: var(--fg); }
1025 .label-tokens .label-entry {
1026 flex: 1 1 9rem;
1027 width: auto;
1028 max-width: none;
1029 min-width: 8rem;
1030 margin: 0;
1031 padding: 2px 0;
1032 background: none;
1033 border: 0;
1034 border-radius: 0;
1035 font-size: 12.5px;
1036 color: var(--fg);
1037 }
1038 .label-tokens .label-entry:focus { outline: none; }
1039 .label-entry::placeholder { color: var(--fg-faint); }
1040 .label-suggest {
1041 position: absolute;
1042 z-index: 30;
1043 top: calc(100% + 4px);
1044 left: 0;
1045 right: 0;
1046 max-height: 12rem;
1047 overflow: auto;
1048 padding: 4px;
1049 background: var(--bg-raised);
1050 border: 1px solid var(--border-strong);
1051 border-radius: var(--radius);
1052 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
1053 }
1054 .label-suggest[hidden], .label-option[hidden] { display: none; }
1055 .label-option {
1056 display: block;
1057 width: 100%;
1058 min-height: 0;
1059 padding: 5px 8px;
1060 background: none;
1061 border: 0;
1062 border-radius: 4px;
1063 color: var(--fg);
1064 text-align: left;
1065 font-size: 12px;
1066 font-weight: 400;
1067 }
1068 .label-option:hover, .label-option:focus { background: var(--bg-hover); filter: none; }
1069
1070 /* Icons are inline SVG from Lucide. They inherit currentColor and size with
1071 the text around them. */
1072 .icon { width: 14px; height: 14px; flex: 0 0 auto; }
1073 .topnav a, .topright .account { display: inline-flex; align-items: center; gap: 5px; }
1074 .tabs a { display: inline-flex; align-items: center; gap: 5px; }
1075 h2 .icon { width: 13px; height: 13px; margin-right: 4px; vertical-align: -2px; color: var(--fg-faint); }
1076
1077 /* A token save is triggered by the token input, not a button. Dim the field
1078 while its request is in flight. */
1079 form.htmx-request .label-tokens { opacity: 0.6; }
1080
1081 /* Line-anchored review comments: a small plus on each line opens the composer. */
1082 .diff td.comment { width: 1.7rem; padding: 0 3px; text-align: center; }
1083 .diff td.comment a {
1084 display: inline-flex;
1085 align-items: center;
1086 justify-content: center;
1087 width: 17px;
1088 height: 17px;
1089 border-radius: 3px;
1090 color: var(--fg-faint);
1091 opacity: 0.3;
1092 }
1093 .diff tr:hover td.comment a,
1094 .diff td.comment a:focus-visible { opacity: 1; background: var(--bg-hover); color: var(--accent); }
1095 .diff td.comment .icon { width: 11px; height: 11px; }
1096 .anchor-note { margin: 0 0 8px; font-size: 12px; }
1097 @media (max-width: 720px) { .diff td.comment a { opacity: .7; } }
1098
1099 /* Repository activity feed. */
1100 .activity-list {
1101 border: 1px solid var(--border);
1102 border-radius: var(--radius-lg);
1103 background: var(--bg-panel);
1104 overflow: hidden;
1105 }
1106 .activity-event {
1107 display: flex;
1108 gap: 10px;
1109 padding: 10px 12px;
1110 border-bottom: 1px solid var(--border);
1111 align-items: flex-start;
1112 }
1113 .activity-event:last-child { border-bottom: 0; }
1114 .activity-icon {
1115 display: grid;
1116 place-items: center;
1117 flex: 0 0 auto;
1118 width: 24px;
1119 height: 24px;
1120 border-radius: 6px;
1121 background: var(--bg-raised);
1122 color: var(--fg-muted);
1123 }
1124 .activity-icon .icon { width: 13px; height: 13px; }
1125 .activity-icon.commit { color: var(--accent); }
1126 .activity-icon.issue { color: var(--good); }
1127 .activity-icon.change { color: #c792ea; }
1128 .activity-icon.release { color: var(--warn); }
1129 .activity-icon.run { color: var(--fg-muted); }
1130 .activity-body { min-width: 0; flex: 1; }
1131 .activity-title {
1132 display: block;
1133 color: var(--fg);
1134 font-weight: 500;
1135 overflow: hidden;
1136 text-overflow: ellipsis;
1137 white-space: nowrap;
1138 }
1139 .activity-title:hover { color: var(--accent); text-decoration: none; }
1140 .activity-meta { color: var(--fg-faint); font-size: 11.5px; margin-top: 2px; }
1141 @media (max-width: 720px) {
1142 .activity-title { white-space: normal; }
1143 }