orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1@use "variables" as *;
2
3// Bootstrap 5.3 writes defaults into `:root,[data-bs-theme=light]`. Use
4// `html:root` so we outrank the default block and enforce the dark palette.
5html:root {
6 --bs-body-bg: #{$body-bg};
7 --bs-body-color: #{$body-color};
8 --bs-body-bg-rgb: 14, 13, 10;
9 --bs-body-color-rgb: 221, 215, 205;
10 --bs-emphasis-color: #{$white};
11 --bs-secondary-bg: #{$card-bg};
12 --bs-tertiary-bg: #18160f;
13 --bs-border-color: rgba(107, 158, 120, 0.3);
14 --bs-link-color-rgb: 125, 184, 140;
15 --bs-link-hover-color-rgb: 149, 204, 162;
16 --bs-heading-color: #{$white};
17}
18
19body {
20 min-height: 100vh;
21 display: flex;
22 flex-direction: column;
23 -webkit-font-smoothing: antialiased;
24 -moz-osx-font-smoothing: grayscale;
25 letter-spacing: 0.01em;
26 background-color: $body-bg !important;
27 color: $body-color;
28
29 // The structural grid the whole estate sits on. 32px because that is the
30 // padding rhythm the panels already use, and low enough alpha that it reads
31 // as texture rather than as a table. Green here where dash is amber, and a
32 // touch more alpha to match, since green at this value reads fainter.
33 background-image:
34 linear-gradient(rgba(107, 158, 120, 0.028) 1px, transparent 1px),
35 linear-gradient(90deg, rgba(107, 158, 120, 0.028) 1px, transparent 1px);
36 background-size: 32px 32px;
37 background-position: center top;
38}
39
40main { flex: 1; }
41
42// The keyboard focus ring, and the selection colour. repos has had both since
43// it was written and the other four had neither, so a tab through a form here
44// depended on whatever Bootstrap happened to do per component.
45:focus-visible {
46 outline: 2px solid $green;
47 outline-offset: 2px;
48 border-radius: 2px;
49}
50
51::selection { background: rgba(107, 158, 120, 0.3); }
52
53
54.vh-100 { height: 100vh; }
55
56.logo {
57 display: inline-flex;
58 align-items: center;
59 justify-content: center;
60 width: 28px;
61 height: 28px;
62 text-decoration: none !important;
63 transition: transform 250ms ease;
64
65 svg {
66 width: 100%;
67 height: 100%;
68 display: block;
69 }
70
71 &:hover {
72 transform: translateY(-1px);
73 }
74
75 &.logo-sm {
76 width: 18px;
77 height: 18px;
78 }
79}
80
81// `min-height` rather than `height`, so the bar grows when the mobile collapse
82// opens. At a fixed height the open menu is painted over by the siblings below.
83
84.navbar {
85 min-height: 52px;
86 padding: 0;
87 background: #090806 !important;
88 border-bottom: 1px solid rgba(107, 158, 120, 0.3);
89
90 > .container {
91 min-height: 52px;
92 }
93
94 .navbar-brand {
95 display: flex;
96 align-items: center;
97 gap: 0.6rem;
98 color: $white;
99 font-weight: 600;
100 letter-spacing: 0.1em;
101 text-transform: uppercase;
102 font-size: 0.78rem;
103 text-decoration: none;
104
105 .brand-text { color: $white; }
106 .brand-dot {
107 display: inline-block;
108 width: 6px; height: 6px; border-radius: 50%;
109 background: $green;
110 }
111 }
112
113 .navbar-toggler {
114 color: rgba(221, 215, 205, 0.7);
115 padding: 0.35rem 0.55rem;
116 font-size: 1rem;
117
118 &:focus { box-shadow: none; }
119 }
120
121 .nav-link {
122 font-size: 0.8rem;
123 font-weight: 500;
124 letter-spacing: 0.05em;
125 text-transform: lowercase;
126 padding-left: 1rem !important;
127 padding-right: 1rem !important;
128 color: rgba(221, 215, 205, 0.6);
129 position: relative;
130 transition: color 200ms ease;
131
132 &::after {
133 content: '';
134 position: absolute;
135 bottom: 2px;
136 left: 1rem;
137 right: 1rem;
138 height: 1px;
139 background: $green;
140 transform: scaleX(0);
141 transition: transform 250ms ease;
142 }
143
144 &:hover,
145 &.active { color: $white; }
146
147 &:hover::after,
148 &.active::after { transform: scaleX(1); }
149 }
150
151 @media (max-width: 767.98px) {
152 .navbar-collapse {
153 width: 100%;
154 border-top: 1px solid rgba(107, 158, 120, 0.3);
155 margin-top: 0.25rem;
156 padding: 0.4rem 0 0.6rem;
157 }
158
159 .navbar-nav {
160 margin-left: 0 !important;
161 width: 100%;
162 }
163
164 .nav-link {
165 padding: 0.65rem 0.75rem !important;
166 border-left: 2px solid transparent;
167 border-radius: 2px;
168 transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
169
170 &::after { display: none; }
171
172 &:hover {
173 background: rgba(221, 215, 205, 0.04);
174 color: $white;
175 }
176
177 &.active {
178 color: $white;
179 border-left-color: $green;
180 background: rgba(107, 158, 120, 0.08);
181 }
182 }
183
184 .navbar-nav.ms-auto {
185 margin-top: 0.5rem;
186 padding-top: 0.6rem;
187 border-top: 1px solid rgba(107, 158, 120, 0.3);
188
189 .nav-item { width: 100%; }
190
191 .btn,
192 form,
193 .d-inline { width: 100%; }
194
195 .btn { width: 100%; }
196 }
197 }
198}
199
200.breadcrumb-bar {
201 background: rgba(107, 158, 120, 0.02);
202 border-bottom: 1px solid rgba(107, 158, 120, 0.2);
203
204 .breadcrumb { font-family: $font-family-monospace; }
205}
206
207.section-label {
208 font-size: 0.7rem;
209 font-weight: 600;
210 letter-spacing: 0.12em;
211 text-transform: uppercase;
212 color: $amber;
213 font-family: $font-family-monospace;
214}
215
216.card {
217 background: $card-bg;
218 border: 1px solid $card-border-color;
219 transition: border-color 250ms ease;
220
221 &:hover { border-color: rgba(107, 158, 120, 0.2); }
222}
223
224.status-dot {
225 display: inline-block;
226 width: 8px; height: 8px;
227 border-radius: 50%;
228 margin-right: 0.55rem;
229 vertical-align: middle;
230 flex-shrink: 0;
231
232 &.status-dot-lg {
233 width: 14px; height: 14px;
234 margin-right: 0;
235 }
236
237 &.is-up { background: $green; box-shadow: 0 0 0 3px rgba(107, 158, 120, 0.12); }
238 &.is-down { background: $terracotta; box-shadow: 0 0 0 3px rgba(196, 112, 85, 0.12); }
239 &.is-warn { background: $amber; box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.12); }
240 &.is-idle { background: #4a443c; }
241}
242
243.toggle-label {
244 font-family: $font-family-monospace;
245 font-size: 0.68rem;
246 font-weight: 600;
247 letter-spacing: 0.12em;
248 text-transform: uppercase;
249 color: $gray-400;
250 cursor: help;
251}
252
253.toggle-pill {
254 display: inline-flex;
255 align-items: stretch;
256 border: 1px solid rgba(107, 158, 120, 0.22);
257 border-radius: 999px;
258 overflow: hidden;
259 cursor: pointer;
260 background: #0e0d0a;
261 margin: 0;
262 user-select: none;
263 line-height: 1;
264
265 input {
266 position: absolute;
267 opacity: 0;
268 pointer-events: none;
269 width: 0; height: 0;
270 }
271
272 .toggle-pill-seg {
273 padding: 0.45rem 0.9rem;
274 font-family: $font-family-monospace;
275 font-size: 0.7rem;
276 font-weight: 600;
277 letter-spacing: 0.08em;
278 text-transform: uppercase;
279 color: $gray-500;
280 transition: background 180ms ease, color 180ms ease;
281 }
282
283 input ~ .toggle-pill-private {
284 background: rgba(201, 168, 76, 0.12);
285 color: #ddc06a;
286 }
287
288 input:checked ~ .toggle-pill-private {
289 background: transparent;
290 color: $gray-500;
291 }
292 input:checked ~ .toggle-pill-public {
293 background: $green-dim;
294 color: $green-bright;
295 }
296
297 &:hover {
298 border-color: rgba(107, 158, 120, 0.35);
299 }
300}
301
302.metric-tile {
303 background: $card-bg;
304 border: 1px solid $card-border-color;
305 border-radius: $border-radius;
306 padding: 1rem 1.15rem;
307 height: 100%;
308 display: flex;
309 flex-direction: column;
310
311 .metric-label {
312 font-size: 0.68rem;
313 font-weight: 600;
314 letter-spacing: 0.12em;
315 text-transform: uppercase;
316 color: $gray-400;
317 }
318
319 .metric-value {
320 font-size: 1.75rem;
321 font-weight: 700;
322 line-height: 1.1;
323 color: $white;
324 margin-top: 0.35rem;
325 letter-spacing: -0.01em;
326 }
327
328 .metric-sub {
329 font-size: 0.72rem;
330 color: $gray-400;
331 margin-top: auto; // Pins sub to the bottom so all tiles baseline-align
332 padding-top: 0.35rem;
333 letter-spacing: 0.02em;
334 }
335
336 &.metric-accent-green {
337 border-color: $green-border;
338 .metric-value { color: $green-bright; }
339 }
340 &.metric-accent-amber {
341 border-color: $amber-border;
342 .metric-value { color: #ddc06a; }
343 }
344 &.metric-accent-danger {
345 border-color: $terracotta-border;
346 .metric-value { color: #d88870; }
347 }
348}
349
350.chip {
351 display: inline-flex;
352 align-items: center;
353 gap: 0.4rem;
354 font-family: $font-family-monospace;
355 font-size: 0.7rem;
356 font-weight: 600;
357 letter-spacing: 0.05em;
358 text-transform: uppercase;
359 padding: 0.3rem 0.6rem;
360 border-radius: 2px;
361 border: 1px solid transparent;
362 line-height: 1;
363
364 &.chip-ok {
365 background: $green-dim;
366 color: $green-bright;
367 border-color: $green-border;
368 }
369 &.chip-down {
370 background: $terracotta-dim;
371 color: #e38871;
372 border-color: $terracotta-border;
373 }
374 &.chip-warn {
375 background: $amber-dim;
376 color: #ddc06a;
377 border-color: $amber-border;
378 }
379 &.chip-info {
380 background: rgba(126, 170, 184, 0.08);
381 color: #9ec5d2;
382 border-color: rgba(126, 170, 184, 0.22);
383 }
384 &.chip-muted {
385 background: rgba(221, 215, 205, 0.04);
386 color: $gray-400;
387 border-color: rgba(221, 215, 205, 0.08);
388 }
389}
390
391footer {
392 padding-top: 4rem;
393 padding-bottom: 3rem;
394 position: relative;
395 background: #090806;
396 border-top: 1px solid rgba(107, 158, 120, 0.3);
397
398 .h5 {
399 font-size: 0.7rem;
400 letter-spacing: 0.12em;
401 text-transform: uppercase;
402 color: $amber;
403 font-weight: 600;
404 }
405
406 p {
407 color: rgba(221, 215, 205, 0.5);
408 font-size: 0.85rem;
409 line-height: 1.75;
410
411 a {
412 color: rgba(221, 215, 205, 0.75);
413 text-decoration: underline;
414 text-underline-offset: 2px;
415
416 &:hover { color: $white; }
417 }
418 }
419
420 .links {
421 padding-top: 2rem;
422 padding-bottom: 1rem;
423 }
424}
425
426.footer-bar {
427 background: #050403;
428 border-top: 1px solid rgba(107, 158, 120, 0.2);
429 padding: 0.8rem 0;
430 color: rgba(221, 215, 205, 0.3);
431 font-size: 0.78rem;
432
433 small { color: rgba(221, 215, 205, 0.3); }
434
435 &-link {
436 color: rgba(221, 215, 205, 0.3);
437 transition: color 200ms ease;
438
439 &:hover { color: $white; }
440 }
441}
442
443.uptime-strip {
444 display: flex;
445 gap: 2px;
446 align-items: flex-end;
447 height: 20px;
448
449 .uptime-tick {
450 flex: 1 1 auto;
451 min-width: 2px;
452 height: 100%;
453 background: #211e1a;
454 border-radius: 1px;
455
456 &.is-up { background: $green; opacity: 0.7; }
457 &.is-down { background: $terracotta; opacity: 0.85; }
458 &.is-warn { background: $amber; opacity: 0.7; }
459 &.is-none { background: #18160f; }
460 }
461}
462
463.terminal-block {
464 background: rgba(9, 8, 6, 0.7);
465 border: 1px solid rgba(107, 158, 120, 0.15);
466 border-radius: $border-radius;
467 font-family: $font-family-monospace;
468 font-size: 0.8rem;
469 color: $green-bright;
470 padding: 1rem 1.2rem;
471 line-height: 1.8;
472
473 .t-line { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
474 .t-prompt { color: $amber; margin-right: 0.4rem; }
475 .t-out { color: $gray-200; }
476 .t-comment { color: $gray-500; }
477 .t-key { color: $amber; }
478 .t-val { color: $green-bright; }
479 .t-cursor {
480 display: inline-block; width: 7px; height: 0.9em; vertical-align: -2px;
481 background: $green-bright;
482 animation: t-blink 1s steps(1) infinite;
483 }
484}
485
486@keyframes t-blink {
487 0%, 50% { opacity: 1; }
488 51%, 100% { opacity: 0; }
489}
490
491.property-row {
492 display: grid;
493 grid-template-columns: minmax(0, 1fr) auto;
494 gap: 1rem 1.5rem;
495 background: $card-bg;
496 border: 1px solid $card-border-color;
497 border-radius: $border-radius;
498 padding: 1rem 1.15rem;
499 margin-bottom: 0.75rem;
500 transition: border-color 200ms ease;
501
502 &:hover { border-color: rgba(107, 158, 120, 0.22); }
503
504 &.is-down { border-color: rgba(196, 112, 85, 0.3); }
505
506 .pr-main {
507 min-width: 0;
508 display: flex;
509 flex-direction: column;
510 gap: 0.35rem;
511 }
512
513 .pr-title {
514 font-weight: 600;
515 color: $white;
516 display: flex;
517 align-items: center;
518 min-width: 0;
519
520 a {
521 color: inherit;
522 text-decoration: none;
523 min-width: 0;
524 overflow: hidden;
525 text-overflow: ellipsis;
526 white-space: nowrap;
527
528 &:hover { color: $green-bright; }
529 }
530 }
531
532 .pr-url {
533 font-size: 0.73rem;
534 color: $gray-400;
535 overflow: hidden;
536 text-overflow: ellipsis;
537 white-space: nowrap;
538 }
539
540 .pr-side {
541 display: flex;
542 flex-direction: column;
543 justify-content: space-between;
544 align-items: flex-end;
545 gap: 0.75rem;
546 min-height: 68px; // Matches the left column's natural height so actions pin to bottom
547 }
548
549 .pr-meta {
550 display: flex;
551 gap: 0.85rem;
552 align-items: flex-end;
553 font-family: $font-family-monospace;
554
555 .pr-meta-cell {
556 display: flex;
557 flex-direction: column;
558 align-items: flex-start;
559 gap: 0.2rem;
560 min-width: 0;
561 }
562
563 .pr-meta-k {
564 color: $gray-500;
565 text-transform: uppercase;
566 font-size: 0.6rem;
567 letter-spacing: 0.1em;
568 line-height: 1;
569 }
570 }
571
572 .pr-actions {
573 display: flex;
574 gap: 0.4rem;
575 }
576}
577
578@media (max-width: 767px) {
579 .property-row {
580 grid-template-columns: 1fr;
581
582 .pr-side {
583 min-height: 0;
584 align-items: stretch;
585 }
586 .pr-meta {
587 flex-wrap: wrap;
588 justify-content: flex-start;
589 }
590 .pr-actions {
591 justify-content: flex-end;
592 }
593 }
594}
595
596.search-form {
597 position: relative;
598
599 .search-icon {
600 position: absolute;
601 left: 0.9rem;
602 top: 50%;
603 transform: translateY(-50%);
604 color: $gray-500;
605 pointer-events: none;
606 z-index: 2;
607 }
608
609 .search-input {
610 padding-left: 2.5rem;
611 font-size: 0.85rem;
612 border: 1px solid rgba(107, 158, 120, 0.15);
613 background: rgba(19, 18, 14, 0.7);
614 color: $body-color;
615 transition: border-color 200ms ease;
616
617 &::placeholder {
618 color: $gray-500;
619 font-style: normal;
620 }
621
622 &:focus {
623 border-color: rgba(107, 158, 120, 0.4);
624 box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.1);
625 background: $card-bg;
626 }
627 }
628}
629
630// Input and button share one visual line, so both take the same 38px height.
631
632.dashboard-toolbar {
633 display: flex;
634 gap: 0.5rem;
635 align-items: stretch;
636
637 .form-control {
638 height: 38px;
639 font-size: 0.85rem;
640 }
641
642 .btn {
643 height: 38px;
644 padding: 0 1.1rem;
645 display: inline-flex;
646 align-items: center;
647 white-space: nowrap;
648 }
649}
650
651.chart-panel {
652 background: $card-bg;
653 border: 1px solid $card-border-color;
654 border-radius: $border-radius;
655 padding: 1rem 1.15rem;
656
657 .chart-panel-header {
658 display: flex;
659 align-items: center;
660 justify-content: space-between;
661 margin-bottom: 0.75rem;
662 }
663
664 .chart-panel-title {
665 font-size: 0.68rem;
666 font-weight: 600;
667 letter-spacing: 0.12em;
668 text-transform: uppercase;
669 color: $amber;
670 font-family: $font-family-monospace;
671 }
672}
673
674.check-list {
675 background: $card-bg;
676 border: 1px solid $card-border-color;
677 border-radius: $border-radius;
678 overflow: hidden;
679
680 .check-list-header {
681 background: rgba(107, 158, 120, 0.04);
682 padding: 0.6rem 1rem;
683 font-size: 0.68rem;
684 font-weight: 600;
685 letter-spacing: 0.12em;
686 text-transform: uppercase;
687 color: $amber;
688 font-family: $font-family-monospace;
689 border-bottom: 1px solid rgba(107, 158, 120, 0.1);
690 }
691
692 .check-list-item {
693 display: flex;
694 justify-content: space-between;
695 align-items: center;
696 padding: 0.55rem 1rem;
697 font-size: 0.85rem;
698 border-bottom: 1px solid rgba(107, 158, 120, 0.2);
699
700 &:last-child { border-bottom: none; }
701
702 .check-name {
703 color: $gray-300;
704 display: flex;
705 align-items: center;
706 gap: 0.5rem;
707 }
708 }
709}
710
711.monitor-card {
712 background: $card-bg;
713 border: 1px solid $card-border-color;
714 border-radius: $border-radius;
715 overflow: hidden;
716 display: flex;
717 flex-direction: column;
718
719 .monitor-header {
720 display: flex;
721 justify-content: space-between;
722 align-items: center;
723 padding: 0.7rem 1rem;
724 background: rgba(107, 158, 120, 0.04);
725 border-bottom: 1px solid rgba(107, 158, 120, 0.3);
726 flex-shrink: 0;
727
728 .monitor-title {
729 font-family: $font-family-monospace;
730 font-size: 0.78rem;
731 font-weight: 600;
732 letter-spacing: 0.06em;
733 text-transform: uppercase;
734 color: $white;
735 }
736 }
737
738 .monitor-body {
739 padding: 0.9rem 1rem;
740 flex: 1 1 auto;
741
742 dl {
743 margin: 0;
744 display: grid;
745 grid-template-columns: max-content 1fr;
746 gap: 0.4rem 1rem;
747 }
748
749 dt {
750 font-size: 0.7rem;
751 letter-spacing: 0.06em;
752 text-transform: uppercase;
753 color: $gray-500;
754 font-weight: 500;
755 }
756
757 dd {
758 margin: 0;
759 font-size: 0.8rem;
760 color: $gray-200;
761 font-family: $font-family-monospace;
762 }
763 }
764}
765
766// Inline flex so the spinner sits centred beside the label instead of dropping
767// off the baseline.
768
769.monitor-btn {
770 display: inline-flex;
771 align-items: center;
772 gap: 0.4rem;
773 padding: 0.2rem 0.65rem;
774 line-height: 1.3;
775
776 .spinner-border-sm {
777 --bs-spinner-width: 0.85rem;
778 --bs-spinner-height: 0.85rem;
779 --bs-spinner-border-width: 0.2em;
780 color: $green-bright;
781 flex-shrink: 0;
782 }
783
784 &:disabled {
785 opacity: 0.85; // Bootstrap's default 0.65 makes the spinner nearly invisible
786 }
787}
788
789.insight-header {
790 display: grid;
791 grid-template-columns: 110px 1fr 1fr 140px;
792 gap: 1rem;
793 padding: 0.55rem 1rem;
794 background: rgba(107, 158, 120, 0.04);
795 border: 1px solid rgba(107, 158, 120, 0.3);
796 border-radius: $border-radius;
797 font-family: $font-family-monospace;
798 font-size: 0.68rem;
799 letter-spacing: 0.08em;
800 text-transform: uppercase;
801 color: $amber;
802 margin-bottom: 0.6rem;
803}
804
805.insight-row {
806 display: grid;
807 grid-template-columns: 110px 1fr 1fr 140px;
808 gap: 1rem;
809 padding: 0.65rem 1rem;
810 background: $card-bg;
811 border: 1px solid $card-border-color;
812 border-radius: $border-radius;
813 margin-bottom: 0.4rem;
814 font-size: 0.85rem;
815 align-items: center;
816
817 &:hover { border-color: rgba(107, 158, 120, 0.2); }
818
819 .insight-col-trunc {
820 min-width: 0;
821 overflow: hidden;
822 text-overflow: ellipsis;
823 white-space: nowrap;
824 }
825}
826
827.insight-header.insight--3col,
828.insight-row.insight--3col {
829 grid-template-columns: 1fr 1fr 140px;
830}
831
832@media (max-width: 767px) {
833 .insight-header { display: none; }
834 .insight-row {
835 grid-template-columns: 1fr;
836 gap: 0.4rem;
837 }
838}
839
840.auth-shell {
841 min-height: calc(100vh - 52px);
842 display: grid;
843 grid-template-columns: 1fr 1fr;
844
845 @media (max-width: 991px) {
846 grid-template-columns: 1fr;
847 }
848
849 .auth-form-col {
850 padding: 3rem 2rem;
851 display: flex;
852 align-items: center;
853 justify-content: center;
854 }
855
856 .auth-visual-col {
857 position: relative;
858 overflow: hidden;
859 background: linear-gradient(145deg, #090806 0%, #0e0d0a 60%, $card-bg 100%);
860 border-left: 1px solid rgba(107, 158, 120, 0.3);
861
862 @media (max-width: 991px) { display: none; }
863 }
864
865 .auth-form-wrap {
866 width: 100%;
867 max-width: 380px;
868 }
869}