A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.
axumdockergitgit-browsergitoxidegixrustself-hosted
1// repos — warm-earth dark palette aligned with analytics + status.
2// Mossy forest green primary + warm amber labels on deep earth darks.
3
4:root {
5 --bg: #0e0d0a;
6 --surface: #13120e;
7 --surface-2: #18160f;
8 --border: rgba(107, 158, 120, 0.12);
9 --border-strong: rgba(107, 158, 120, 0.22);
10 --text: #ddd7cd;
11 --text-mute: #847c72;
12 --text-dim: #665f56;
13 --accent: #6b9e78; // mossy green primary
14 --accent-2: #c9a84c; // warm amber (section labels, callouts)
15 --green: #7db88c; // brighter green for links, hover, active
16 --code-bg: #090806;
17 --add-bg: rgba(107, 158, 120, 0.12);
18 --add-fg: #7db88c;
19 --del-bg: rgba(196, 112, 85, 0.12);
20 --del-fg: #e38871;
21 --hunk-fg: #9ec5d2;
22
23 --mono: "Monaspace Argon", ui-monospace, "Cascadia Code", Consolas, "SF Mono", Menlo, monospace;
24 --prose: "Monaspace Argon", ui-monospace, "Cascadia Code", Consolas, "SF Mono", Menlo, monospace;
25}
26
27* { box-sizing: border-box; }
28
29html, body {
30 margin: 0;
31 padding: 0;
32 background: var(--bg);
33 color: var(--text);
34 font-family: var(--prose);
35 font-size: 0.92rem;
36 line-height: 1.7;
37 letter-spacing: 0.01em;
38 -webkit-font-smoothing: antialiased;
39 -moz-osx-font-smoothing: grayscale;
40}
41
42// Sticky footer: body fills viewport, main takes the slack so the
43// footer-bar pins to the bottom on short pages (empty repo list, 404).
44body {
45 min-height: 100vh;
46 display: flex;
47 flex-direction: column;
48}
49
50main { flex: 1 0 auto; }
51
52a {
53 color: var(--green);
54 text-decoration: none;
55
56 &:hover { text-decoration: underline; }
57}
58
59code, pre, .sha, .clone-box input {
60 font-family: var(--mono);
61}
62
63.container {
64 max-width: 1100px;
65 margin: 0 auto;
66 padding: 0 1.25rem;
67}
68
69// --- topbar ---
70.topbar {
71 position: relative;
72 background: #090806;
73 border-bottom: 1px solid rgba(107, 158, 120, 0.06);
74 padding: 0.85rem 0;
75
76 &__row {
77 display: flex;
78 align-items: center;
79 gap: 1rem;
80 flex-wrap: wrap;
81 }
82
83 // 1px vertical hairline between brand and tagline, like a path-component
84 // separator. Hidden on narrow screens where the tagline drops out anyway.
85 &__divider {
86 width: 1px;
87 height: 1.4rem;
88 background: var(--border);
89
90 @media (max-width: 540px) { display: none; }
91 }
92}
93
94.brand {
95 display: inline-flex;
96 align-items: center;
97 gap: 0.6rem;
98 color: var(--text);
99 font-family: var(--mono);
100 font-weight: 600;
101 font-size: 0.78rem;
102 text-transform: uppercase;
103 letter-spacing: 0.1em;
104 text-decoration: none;
105
106 &:hover {
107 text-decoration: none;
108 color: var(--text);
109
110 .brand-mark { transform: translateY(-1px); }
111 }
112}
113
114.brand-mark {
115 display: inline-flex;
116 width: 22px;
117 height: 22px;
118 color: var(--accent);
119 transition: transform 250ms ease;
120
121 svg { width: 100%; height: 100%; display: block; }
122}
123
124.brand-text { color: var(--text); }
125
126.tagline {
127 color: var(--text-mute);
128 font-family: var(--mono);
129 font-style: italic;
130 font-size: 0.85rem;
131 letter-spacing: 0.02em;
132
133 @media (max-width: 540px) { display: none; }
134}
135
136.topnav {
137 margin-left: auto;
138 display: inline-flex;
139 align-items: center;
140 gap: 0.25rem;
141
142 &__link {
143 display: inline-flex;
144 align-items: center;
145 gap: 0.5rem;
146 color: var(--text-mute);
147 font-family: var(--mono);
148 font-size: 0.85rem;
149 text-transform: lowercase;
150 letter-spacing: 0.04em;
151 padding: 0.35rem 0.75rem;
152 border: 1px solid transparent;
153 border-radius: 3px;
154 transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
155
156 &:hover {
157 color: var(--text);
158 border-color: var(--border);
159 background: var(--surface-2);
160 text-decoration: none;
161 }
162
163 // Current-page state: a subtle outlined "tab" with the accent dot lit.
164 &.is-active {
165 color: var(--text);
166 border-color: var(--border);
167 background: var(--surface-2);
168
169 .topnav__dot {
170 background: var(--accent);
171 box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.15);
172 }
173 }
174 }
175
176 &__dot {
177 width: 6px;
178 height: 6px;
179 border-radius: 50%;
180 background: var(--text-mute);
181 opacity: 0.6;
182 }
183}
184
185// --- breadcrumbs ---
186.breadcrumbs {
187 background: var(--surface);
188 border-bottom: 1px solid var(--border);
189 font-family: var(--mono);
190 font-size: 0.9rem;
191 color: var(--text-mute);
192 padding: 0.85rem 0;
193 // A deep path with a long component must wrap, not scroll the page.
194 overflow-wrap: anywhere;
195
196 strong { color: var(--text); font-weight: 600; }
197
198 // Explicit separator span so we don't depend on whitespace stripping in
199 // the templates. Each path component is its own anchor; the slashes are
200 // visual-only and get a touch of horizontal margin.
201 &__sep {
202 margin: 0 0.35rem;
203 color: var(--text-mute);
204 opacity: 0.6;
205 }
206
207 &__ref {
208 margin-left: 0.75rem;
209 color: var(--accent-2);
210 }
211}
212
213main { padding: 3rem 0 4rem; }
214
215// --- hero / index ---
216.hero {
217 margin-bottom: 2.5rem;
218
219 h1 {
220 font-family: var(--mono);
221 font-size: 2rem;
222 margin: 0 0 0.5rem;
223 letter-spacing: -0.01em;
224 }
225
226 .lede { color: var(--text-mute); margin: 0; }
227}
228
229.empty {
230 color: var(--text-mute);
231 font-style: italic;
232}
233
234.repo-list {
235 list-style: none;
236 padding: 0;
237 margin: 0;
238 display: grid;
239 gap: 1px;
240 background: var(--border);
241 border: 1px solid var(--border);
242 border-radius: 4px;
243 overflow: hidden;
244}
245
246.repo-row {
247 background: var(--surface);
248 padding: 1rem 1.25rem;
249
250 &:hover { background: var(--surface-2); }
251
252 &__head {
253 display: flex;
254 justify-content: space-between;
255 align-items: baseline;
256 gap: 1rem;
257 }
258
259 &__name {
260 font-family: var(--mono);
261 font-weight: 600;
262 font-size: 1.1rem;
263 color: var(--green);
264 }
265
266 &__age {
267 color: var(--text-mute);
268 font-size: 0.85rem;
269 white-space: nowrap;
270 }
271
272 &__desc {
273 margin: 0.35rem 0 0;
274 color: var(--text);
275 }
276
277 &__last {
278 margin: 0.35rem 0 0;
279 color: var(--text-mute);
280 font-size: 0.88rem;
281 }
282
283 &__last-sha {
284 font-family: var(--mono);
285 color: var(--accent-2);
286 margin-right: 0.4rem;
287 }
288}
289
290// --- repo head ---
291.repo-head {
292 margin-bottom: 2rem;
293
294 &__title-row {
295 display: flex;
296 align-items: baseline;
297 gap: 0.75rem;
298 flex-wrap: wrap;
299
300 h1 {
301 font-family: var(--mono);
302 font-size: 1.75rem;
303 margin: 0;
304 }
305 }
306
307 &__branch {
308 background: var(--surface-2);
309 color: var(--accent-2);
310 border: 1px solid var(--border);
311 padding: 0.1rem 0.5rem;
312 border-radius: 3px;
313 font-family: var(--mono);
314 font-size: 0.85rem;
315 }
316
317 &__desc {
318 color: var(--text-mute);
319 margin: 0.5rem 0 1rem;
320 }
321}
322
323.clone-box {
324 display: flex;
325 align-items: center;
326 gap: 0.75rem;
327 background: var(--surface);
328 border: 1px solid var(--border);
329 border-radius: 4px;
330 padding: 0.5rem 0.75rem;
331 margin-bottom: 1rem;
332 max-width: 640px;
333
334 label {
335 color: var(--text-mute);
336 font-size: 0.85rem;
337 text-transform: uppercase;
338 letter-spacing: 0.05em;
339 }
340
341 input {
342 flex: 1;
343 background: transparent;
344 border: 0;
345 color: var(--text);
346 font-size: 0.9rem;
347 outline: none;
348 padding: 0.25rem 0;
349 }
350}
351
352.repo-nav {
353 display: flex;
354 gap: 1.25rem;
355 border-bottom: 1px solid var(--border);
356 padding-bottom: 0.5rem;
357 font-family: var(--mono);
358 font-size: 0.92rem;
359
360 a {
361 color: var(--text-mute);
362 &:hover { color: var(--green); text-decoration: none; }
363 }
364}
365
366// --- two column layout (overview) ---
367.two-col {
368 display: grid;
369 // `minmax(0, ...)` is load-bearing: CSS grid items default to min-width:
370 // auto, so an unwrappable child (e.g. a wide <pre> inside the README)
371 // would force the main column wider than its 2fr share and crush the
372 // sidebar. Capping the min at 0 makes the columns respect the fr ratio
373 // regardless of content width; overflow scrolls within the child.
374 grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
375 gap: 2rem;
376
377 @media (max-width: 760px) {
378 grid-template-columns: minmax(0, 1fr);
379 }
380}
381
382.col-main { min-width: 0; }
383.col-side { min-width: 0; }
384
385.side-h {
386 font-family: var(--mono);
387 font-size: 0.85rem;
388 text-transform: uppercase;
389 letter-spacing: 0.06em;
390 color: var(--text-mute);
391 margin: 0 0 0.5rem;
392}
393
394.commit-list {
395 list-style: none;
396 margin: 0;
397 padding: 0;
398
399 li {
400 border-bottom: 1px dashed var(--border);
401 padding: 0.55rem 0;
402 font-size: 0.92rem;
403 }
404
405 .sha {
406 color: var(--accent-2);
407 margin-right: 0.4rem;
408 }
409
410 .summary { display: block; color: var(--text); }
411 .meta { color: var(--text-mute); font-size: 0.82rem; }
412}
413
414// --- readme ---
415.readme {
416 background: var(--surface);
417 border: 1px solid var(--border);
418 border-radius: 4px;
419 padding: 1.5rem 2rem;
420 // Long URLs and other unbreakable tokens wrap instead of widening the
421 // page; the README is user content so we can't assume it stays narrow.
422 overflow-wrap: break-word;
423
424 // Tighter gutters on phones — 2rem of padding plus the container's own
425 // 1.25rem leaves too little room for prose on a 375px screen.
426 @media (max-width: 540px) { padding: 1.15rem 1.15rem; }
427
428 h1, h2, h3, h4 {
429 font-family: var(--mono);
430 letter-spacing: -0.01em;
431 }
432 h1 { font-size: 1.7rem; margin-top: 0; }
433 h2 { font-size: 1.35rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
434 h3 { font-size: 1.1rem; }
435
436 p, ul, ol { line-height: 1.65; }
437 code { background: var(--code-bg); padding: 0.1rem 0.35rem; border-radius: 3px; font-size: 0.92em; }
438 pre {
439 background: var(--code-bg);
440 padding: 1rem;
441 border-radius: 4px;
442 overflow-x: auto;
443 code { background: transparent; padding: 0; }
444 }
445 blockquote {
446 border-left: 3px solid var(--accent);
447 padding-left: 1rem;
448 color: var(--text-mute);
449 margin: 1rem 0;
450 }
451 // A markdown table can easily be wider than a phone screen. `display:
452 // block` turns the <table> itself into a horizontal scroll container
453 // (the rows still lay out as a table inside an anonymous table box), so
454 // a wide table scrolls on its own instead of forcing the whole page
455 // wide. `width: max-content` keeps a narrow table from stretching.
456 table {
457 display: block;
458 width: max-content;
459 max-width: 100%;
460 overflow-x: auto;
461 border-collapse: collapse;
462 margin: 1rem 0;
463 th, td {
464 border: 1px solid var(--border);
465 padding: 0.35rem 0.75rem;
466 }
467 th { background: var(--surface-2); }
468 }
469 hr { border: 0; border-top: 1px solid var(--border); }
470}
471
472// --- log ---
473.log-list {
474 list-style: none;
475 padding: 0;
476 margin: 0;
477}
478
479.log-row {
480 display: flex;
481 gap: 1rem;
482 padding: 0.75rem 0;
483 border-bottom: 1px solid var(--border);
484
485 .sha { color: var(--accent-2); flex-shrink: 0; }
486 .summary { margin: 0; }
487 .meta { margin: 0; color: var(--text-mute); font-size: 0.85rem; }
488}
489
490// --- tree ---
491.tree {
492 width: 100%;
493 border-collapse: collapse;
494 font-family: var(--mono);
495 font-size: 0.92rem;
496
497 tr { border-bottom: 1px solid var(--border); }
498 td { padding: 0.5rem 0.75rem; }
499
500 &__name { width: 100%; overflow-wrap: anywhere; }
501 &__name--tree a::before { content: ""; color: var(--accent-2); }
502 &__name--blob a::before { content: ""; color: var(--text-mute); }
503 &__size { color: var(--text-mute); text-align: right; white-space: nowrap; }
504}
505
506// --- blob view ---
507.blob-head {
508 display: flex;
509 justify-content: space-between;
510 align-items: center;
511 background: var(--surface);
512 border: 1px solid var(--border);
513 border-bottom: 0;
514 padding: 0.5rem 0.75rem;
515 border-radius: 4px 4px 0 0;
516 font-size: 0.85rem;
517 color: var(--text-mute);
518 font-family: var(--mono);
519}
520
521.blob {
522 border: 1px solid var(--border);
523 border-radius: 0 0 4px 4px;
524 overflow-x: auto;
525 background: var(--code-bg);
526
527 .hl {
528 margin: 0;
529 padding: 0.75rem 0;
530 font-family: var(--mono);
531 font-size: 0.85rem;
532 line-height: 1.55;
533 }
534 .ln {
535 display: inline-block;
536 width: 4ch;
537 padding-right: 1rem;
538 text-align: right;
539 color: var(--text-mute);
540 user-select: none;
541 &::before { content: attr(data-n); }
542 }
543 .l { display: inline; }
544}
545
546// --- diff ---
547.commit-head {
548 margin-bottom: 1.5rem;
549
550 h1 {
551 font-family: var(--mono);
552 font-size: 1.35rem;
553 margin: 0 0 0.5rem;
554 }
555 .sha { color: var(--accent-2); }
556 .meta { color: var(--text-mute); margin: 0 0 1rem; }
557}
558
559.commit-msg {
560 background: var(--surface);
561 border: 1px solid var(--border);
562 padding: 0.75rem 1rem;
563 border-radius: 4px;
564 font-size: 0.9rem;
565 white-space: pre-wrap;
566 margin: 0;
567}
568
569.file-diff {
570 margin-top: 1.5rem;
571 border: 1px solid var(--border);
572 border-radius: 4px;
573 overflow: hidden;
574
575 &__head {
576 background: var(--surface);
577 padding: 0.5rem 0.75rem;
578 display: flex;
579 align-items: center;
580 // Let a long file path wrap below the status badge instead of being
581 // clipped by this article's `overflow: hidden`.
582 flex-wrap: wrap;
583 gap: 0.35rem 0.75rem;
584 font-family: var(--mono);
585 font-size: 0.88rem;
586 }
587
588 .path { overflow-wrap: anywhere; min-width: 0; }
589}
590
591.status {
592 font-family: var(--mono);
593 font-size: 0.75rem;
594 text-transform: uppercase;
595 padding: 0.05rem 0.45rem;
596 border-radius: 3px;
597 letter-spacing: 0.05em;
598
599 &--added { background: rgba(125, 184, 140, 0.15); color: var(--add-fg); }
600 &--deleted { background: rgba(196, 112, 85, 0.15); color: var(--del-fg); }
601 &--modified { background: var(--surface-2); color: var(--text-mute); }
602 &--renamed { background: rgba(201, 168, 76, 0.15); color: var(--accent-2); }
603}
604
605.hunk {
606 margin: 0;
607 padding: 0.5rem 0;
608 background: var(--code-bg);
609 font-family: var(--mono);
610 font-size: 0.82rem;
611 line-height: 1.35;
612 overflow-x: auto;
613}
614
615.hunk-hdr {
616 display: block;
617 padding: 0.15rem 0.75rem;
618 color: var(--hunk-fg);
619 background: rgba(158, 197, 210, 0.08);
620}
621
622.line {
623 display: block;
624 padding: 0 0.75rem;
625 white-space: pre;
626
627 &--add { background: var(--add-bg); color: var(--add-fg);
628 &::before { content: "+"; display: inline-block; width: 1.2ch; color: var(--add-fg); opacity: 0.6; }
629 }
630 &--del { background: var(--del-bg); color: var(--del-fg);
631 &::before { content: "-"; display: inline-block; width: 1.2ch; color: var(--del-fg); opacity: 0.6; }
632 }
633 &--ctx { color: var(--text);
634 &::before { content: " "; display: inline-block; width: 1.2ch; }
635 }
636}
637
638// --- footer ---
639.footer {
640 margin-top: 4rem;
641 padding: 4rem 0 3rem;
642 border-top: 1px solid rgba(107, 158, 120, 0.06);
643 background: #090806;
644
645 &__grid {
646 display: grid;
647 gap: 2.5rem;
648 grid-template-columns: minmax(0, 2.4fr) minmax(0, 1fr) minmax(0, 1fr);
649 position: relative;
650 z-index: 1;
651
652 @media (max-width: 760px) {
653 grid-template-columns: minmax(0, 1fr);
654 gap: 2rem;
655 }
656 }
657
658 &__label {
659 font-family: var(--mono);
660 font-size: 0.75rem;
661 font-weight: 600;
662 letter-spacing: 0.12em;
663 text-transform: uppercase;
664 color: var(--accent-2);
665 opacity: 0.7;
666 margin-bottom: 0.9rem;
667 }
668
669 &__about p {
670 color: var(--text-mute);
671 font-size: 0.9rem;
672 line-height: 1.7;
673 margin: 0 0 0.85rem;
674 max-width: 56ch;
675
676 a {
677 color: var(--text);
678 text-decoration: underline;
679 text-underline-offset: 2px;
680 text-decoration-color: var(--border);
681
682 &:hover { color: var(--accent-2); text-decoration-color: var(--accent-2); }
683 }
684 }
685
686 &__tagline {
687 font-style: italic;
688 color: var(--text-mute);
689 opacity: 0.75;
690 }
691
692 &__col {
693 ul {
694 list-style: none;
695 margin: 0;
696 padding: 0;
697 }
698
699 li { margin-bottom: 0.55rem; }
700
701 a {
702 color: var(--text-mute);
703 font-size: 0.9rem;
704 transition: color 150ms ease;
705
706 &:hover { color: var(--accent-2); text-decoration: none; }
707 }
708 }
709
710}
711
712// Slim copyright bar, like blog/status/analytics.
713.footer-bar {
714 background: #050403;
715 border-top: 1px solid rgba(107, 158, 120, 0.04);
716 padding: 0.8rem 0;
717 color: rgba(221, 215, 205, 0.3);
718 font-size: 0.78rem;
719
720 &__row {
721 display: flex;
722 align-items: center;
723 justify-content: space-between;
724 gap: 1rem;
725
726 @media (max-width: 480px) {
727 flex-direction: column;
728 gap: 0.6rem;
729 text-align: center;
730 }
731 }
732
733 &__link {
734 display: inline-flex;
735 color: var(--text-mute);
736 transition: color 150ms ease;
737
738 &:hover { color: var(--accent-2); }
739 }
740}