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: #1c1a15;
12 --bs-tertiary-bg: #201d16;
13 --bs-border-color: rgba(107, 158, 120, 0.42);
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
57.logo {
58 display: inline-flex;
59 align-items: center;
60 justify-content: center;
61 width: 28px;
62 height: 28px;
63 text-decoration: none !important;
64 transition: transform 250ms ease;
65
66 svg {
67 width: 100%;
68 height: 100%;
69 display: block;
70 }
71
72 &:hover {
73 transform: translateY(-1px);
74 }
75
76 &.logo-sm {
77 width: 18px;
78 height: 18px;
79 }
80}
81
82// `min-height` rather than `height`, so the bar grows when the mobile collapse
83// opens. At a fixed height the open menu is painted over by the siblings below.
84
85.navbar {
86 min-height: 52px;
87 padding: 0;
88 background: #090806 !important;
89 border-bottom: 1px solid rgba(107, 158, 120, 0.2);
90
91 > .container {
92 min-height: 52px;
93 }
94
95 .navbar-brand {
96 display: flex;
97 align-items: center;
98 gap: 0.55rem;
99 // Without this the text item is as tall as the inherited 1.7 line-height,
100 // and centring a 21px box against a 22px mark is what left them looking a
101 // pixel out from each other.
102 line-height: 1;
103 color: $white;
104 font-weight: 600;
105 letter-spacing: 0.1em;
106 text-transform: uppercase;
107 font-size: 0.78rem;
108 text-decoration: none;
109
110 .logo {
111 width: 22px;
112 height: 22px;
113 }
114
115 .brand-text {
116 display: block;
117 color: $white;
118 // All caps has no descenders, so the glyphs sit above the middle of
119 // their own box. Nudging the text rather than the marks keeps the star
120 // and the dot on the true centre line.
121 transform: translateY(0.055em);
122 }
123
124 .brand-dot {
125 display: block;
126 width: 5px;
127 height: 5px;
128 border-radius: 50%;
129 background: $green;
130 flex: none;
131 }
132 }
133
134 .navbar-toggler {
135 color: rgba(221, 215, 205, 0.85);
136 padding: 0.35rem 0.55rem;
137 font-size: 1rem;
138
139 &:focus { box-shadow: none; }
140 }
141
142 .nav-link {
143 font-size: 0.8rem;
144 font-weight: 500;
145 letter-spacing: 0.05em;
146 text-transform: lowercase;
147 padding-left: 1rem !important;
148 padding-right: 1rem !important;
149 color: rgba(221, 215, 205, 0.8);
150 position: relative;
151 transition: color 200ms ease;
152
153 &::after {
154 content: '';
155 position: absolute;
156 bottom: 2px;
157 left: 1rem;
158 right: 1rem;
159 height: 1px;
160 background: $green;
161 transform: scaleX(0);
162 transition: transform 250ms ease;
163 }
164
165 &:hover,
166 &.active { color: $white; }
167
168 &:hover::after,
169 &.active::after { transform: scaleX(1); }
170 }
171
172 @media (max-width: 767.98px) {
173 .navbar-collapse {
174 width: 100%;
175 border-top: 1px solid rgba(107, 158, 120, 0.28);
176 margin-top: 0.25rem;
177 padding: 0.4rem 0 0.6rem;
178 }
179
180 .navbar-nav {
181 margin-left: 0 !important;
182 width: 100%;
183 }
184
185 .nav-link {
186 padding: 0.65rem 0.75rem !important;
187 border-left: 2px solid transparent;
188 border-radius: 2px;
189 transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
190
191 &::after { display: none; }
192
193 &:hover {
194 background: rgba(221, 215, 205, 0.09);
195 color: $white;
196 }
197
198 &.active {
199 color: $white;
200 border-left-color: $green;
201 background: rgba(107, 158, 120, 0.16);
202 }
203 }
204
205 .navbar-nav.ms-auto {
206 margin-top: 0.5rem;
207 padding-top: 0.6rem;
208 border-top: 1px solid rgba(107, 158, 120, 0.24);
209
210 .nav-item { width: 100%; }
211
212 .btn,
213 form,
214 .d-inline { width: 100%; }
215
216 .btn { width: 100%; }
217 }
218 }
219}
220
221.breadcrumb-bar {
222 background: rgba(107, 158, 120, 0.05);
223 border-bottom: 1px solid rgba(107, 158, 120, 0.2);
224
225 .breadcrumb { font-family: $font-family-monospace; }
226}
227
228.section-label {
229 font-size: 0.7rem;
230 font-weight: 600;
231 letter-spacing: 0.12em;
232 text-transform: uppercase;
233 color: rgba(201, 168, 76, 0.95);
234 font-family: $font-family-monospace;
235}
236
237.card {
238 background: $card-bg;
239 border: 1px solid $card-border-color;
240 transition: border-color 250ms ease;
241
242 &:hover { border-color: rgba(107, 158, 120, 0.2); }
243}
244
245.status-dot {
246 display: inline-block;
247 width: 8px; height: 8px;
248 border-radius: 50%;
249 margin-right: 0.55rem;
250 vertical-align: middle;
251 flex-shrink: 0;
252
253 &.status-dot-lg {
254 width: 14px; height: 14px;
255 margin-right: 0;
256 }
257
258 &.is-up { background: $green; box-shadow: 0 0 0 3px rgba(107, 158, 120, 0.25); }
259 &.is-down { background: $terracotta; box-shadow: 0 0 0 3px rgba(196, 112, 85, 0.25); }
260 &.is-warn { background: $amber; box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.25); }
261 &.is-idle { background: #4a443c; }
262}
263
264.metric-tile {
265 background: $card-bg;
266 border: 1px solid $card-border-color;
267 border-radius: $border-radius;
268 padding: 1rem 1.15rem;
269 height: 100%;
270 display: flex;
271 flex-direction: column;
272
273 .metric-header {
274 display: flex;
275 align-items: center;
276 gap: 0.5rem;
277 min-height: 1.3rem;
278 }
279
280 .metric-label {
281 flex: 1;
282 min-width: 0;
283 font-size: 0.68rem;
284 font-weight: 600;
285 letter-spacing: 0.12em;
286 text-transform: uppercase;
287 color: #9a9188;
288 }
289
290 .metric-value {
291 font-size: 1.75rem;
292 font-weight: 700;
293 line-height: 1.1;
294 color: $white;
295 margin-top: 0.35rem;
296 letter-spacing: -0.01em;
297 word-break: break-word;
298 }
299
300 .metric-sub {
301 font-size: 0.72rem;
302 color: #9a9188;
303 margin-top: auto;
304 padding-top: 0.35rem;
305 letter-spacing: 0.02em;
306 }
307
308 .metric-delta {
309 flex-shrink: 0;
310 font-family: $font-family-monospace;
311 font-size: 0.75rem;
312 font-weight: 600;
313 letter-spacing: 0.04em;
314 padding: 0.22rem 0.5rem;
315 border-radius: 2px;
316 border: 1px solid transparent;
317 line-height: 1;
318
319 &.is-up {
320 color: $green-bright;
321 background: $green-dim;
322 border-color: $green-border;
323 }
324 &.is-down {
325 color: #e38871;
326 background: $terracotta-dim;
327 border-color: $terracotta-border;
328 }
329 &.is-flat {
330 color: #9a9188;
331 background: rgba(221, 215, 205, 0.09);
332 border-color: rgba(221, 215, 205, 0.28);
333 }
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.16);
381 color: #9ec5d2;
382 border-color: rgba(126, 170, 184, 0.5);
383 }
384 &.chip-muted {
385 background: rgba(221, 215, 205, 0.09);
386 color: #9a9188;
387 border-color: rgba(221, 215, 205, 0.28);
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.24);
397
398 .h5 {
399 font-size: 0.7rem;
400 letter-spacing: 0.12em;
401 text-transform: uppercase;
402 color: rgba(201, 168, 76, 0.85);
403 font-weight: 600;
404 }
405
406 p {
407 color: rgba(221, 215, 205, 0.68);
408 font-size: 0.85rem;
409 line-height: 1.75;
410
411 a {
412 color: rgba(221, 215, 205, 0.88);
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.58);
431 font-size: 0.78rem;
432
433 small { color: rgba(221, 215, 205, 0.58); }
434
435 &-link {
436 color: rgba(221, 215, 205, 0.58);
437 transition: color 200ms ease;
438
439 &:hover { color: $white; }
440 }
441}
442
443.terminal-block {
444 background: rgba(9, 8, 6, 0.7);
445 border: 1px solid rgba(107, 158, 120, 0.4);
446 border-radius: $border-radius;
447 font-family: $font-family-monospace;
448 font-size: 0.8rem;
449 color: $green-bright;
450 padding: 1rem 1.2rem;
451 line-height: 1.8;
452
453 .t-line { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
454 .t-prompt { color: $amber; margin-right: 0.4rem; }
455 .t-out { color: $gray-200; }
456 .t-comment { color: #8a8279; }
457 .t-key { color: $amber; }
458 .t-val { color: $green-bright; }
459 .t-cursor {
460 display: inline-block; width: 7px; height: 0.9em; vertical-align: -2px;
461 background: $green-bright;
462 animation: t-blink 1s steps(1) infinite;
463 }
464}
465
466@keyframes t-blink {
467 0%, 50% { opacity: 1; }
468 51%, 100% { opacity: 0; }
469}
470
471.search-form {
472 position: relative;
473
474 .search-icon {
475 position: absolute;
476 left: 0.9rem;
477 top: 50%;
478 transform: translateY(-50%);
479 color: #8a8279;
480 pointer-events: none;
481 z-index: 2;
482 }
483
484 .search-input {
485 padding-left: 2.5rem;
486 font-size: 0.85rem;
487 border: 1px solid rgba(107, 158, 120, 0.4);
488 background: rgba(19, 18, 14, 0.7);
489 color: $body-color;
490 transition: border-color 200ms ease;
491
492 &::placeholder {
493 color: #8a8279;
494 font-style: normal;
495 }
496
497 &:focus {
498 border-color: rgba(107, 158, 120, 0.4);
499 box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.1);
500 background: $card-bg;
501 }
502 }
503}
504
505.dashboard-toolbar {
506 display: flex;
507 gap: 0.5rem;
508 align-items: stretch;
509
510 .form-control {
511 height: 38px;
512 font-size: 0.85rem;
513 }
514
515 .btn {
516 height: 38px;
517 padding: 0 1.1rem;
518 display: inline-flex;
519 align-items: center;
520 white-space: nowrap;
521 }
522}
523
524.chart-panel {
525 background: $card-bg;
526 border: 1px solid $card-border-color;
527 border-radius: $border-radius;
528 padding: 1rem 1.15rem;
529
530 .chart-panel-header {
531 display: flex;
532 align-items: center;
533 justify-content: space-between;
534 margin-bottom: 0.75rem;
535 gap: 0.75rem;
536 }
537
538 .chart-panel-title {
539 font-size: 0.68rem;
540 font-weight: 600;
541 letter-spacing: 0.12em;
542 text-transform: uppercase;
543 color: rgba(201, 168, 76, 0.95);
544 font-family: $font-family-monospace;
545 }
546
547 .chart-panel-body {
548 position: relative;
549 }
550}
551
552
553.rank-list {
554 background: $card-bg;
555 border: 1px solid $card-border-color;
556 border-radius: $border-radius;
557 overflow: hidden;
558 height: 100%;
559 display: flex;
560 flex-direction: column;
561
562 .rank-list-header {
563 display: flex;
564 justify-content: space-between;
565 align-items: center;
566 background: rgba(107, 158, 120, 0.1);
567 padding: 0.6rem 1rem;
568 border-bottom: 1px solid rgba(107, 158, 120, 0.28);
569
570 .rank-list-title {
571 font-size: 0.68rem;
572 font-weight: 600;
573 letter-spacing: 0.12em;
574 text-transform: uppercase;
575 color: rgba(201, 168, 76, 0.95);
576 font-family: $font-family-monospace;
577 }
578
579 .rank-list-col {
580 font-size: 0.62rem;
581 font-weight: 600;
582 letter-spacing: 0.1em;
583 text-transform: uppercase;
584 color: #8a8279;
585 font-family: $font-family-monospace;
586 }
587 }
588
589 .rank-list-body {
590 display: flex;
591 flex-direction: column;
592 flex: 1 1 auto;
593 }
594
595 .rank-list-row {
596 display: grid;
597 grid-template-columns: minmax(0, 1fr) auto;
598 gap: 0.75rem;
599 align-items: center;
600 padding: 0.5rem 1rem;
601 border-bottom: 1px solid rgba(107, 158, 120, 0.2);
602 font-size: 0.85rem;
603
604 &:last-child { border-bottom: none; }
605
606 .rank-label {
607 min-width: 0;
608 overflow: hidden;
609 text-overflow: ellipsis;
610 white-space: nowrap;
611 color: $gray-200;
612
613 a {
614 color: inherit;
615 text-decoration: none;
616
617 &:hover {
618 color: $green-bright;
619 text-decoration: underline;
620 }
621 }
622 }
623
624 .rank-count {
625 font-family: $font-family-monospace;
626 font-size: 0.78rem;
627 color: $amber;
628 font-weight: 600;
629 }
630 }
631
632}
633
634// Not nested inside .rank-list. The same class marks the empty states inside
635// .log-body, which a nested selector never reaches.
636.rank-list-empty {
637 padding: 1.25rem 1rem;
638 color: $gray-300;
639 font-size: 0.82rem;
640 text-align: center;
641}
642
643.auth-shell {
644 min-height: calc(100vh - 52px);
645 display: grid;
646 grid-template-columns: 1fr 1fr;
647
648 @media (max-width: 991px) {
649 grid-template-columns: 1fr;
650 }
651
652 .auth-form-col {
653 padding: 3rem 2rem;
654 display: flex;
655 align-items: center;
656 justify-content: center;
657 }
658
659 .auth-visual-col {
660 position: relative;
661 overflow: hidden;
662 background: linear-gradient(145deg, #090806 0%, #0e0d0a 60%, #13120e 100%);
663 border-left: 1px solid rgba(107, 158, 120, 0.28);
664
665 @media (max-width: 991px) { display: none; }
666 }
667
668 .auth-form-wrap {
669 width: 100%;
670 max-width: 380px;
671 }
672}
673
674
675
676// `danger` above is the same colour under the shared name, kept for markup
677// that still asks for it. slate is for a number that is neither good nor bad.
678.metric-tile {
679 &.metric-accent-terracotta {
680 border-color: $terracotta-border;
681 .metric-value { color: #d88870; }
682 }
683 &.metric-accent-slate {
684 border-color: rgba(126, 170, 184, 0.5);
685 .metric-value { color: #9ec5d2; }
686 }
687}
688
689.text-terracotta { color: #d88870 !important; }
690
691// Absolutely positioned behind the row's text rather than sized as a grid
692// column, so a long label keeps the full width and the count is never pushed off.
693.rank-list .rank-list-row {
694 position: relative;
695
696 .rank-bar {
697 position: absolute;
698 left: 0;
699 top: 0;
700 bottom: 0;
701 background: rgba(107, 158, 120, 0.16);
702 pointer-events: none;
703 z-index: 0;
704 }
705
706 .rank-label,
707 .rank-count {
708 position: relative;
709 z-index: 1;
710 }
711}