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:root {
2 --color-primary: #20232e;
3 --color-blue: #0e3ff4;
4 --color-purple: #842bff;
5 --radius-2: 3px;
6 --transition-250: 250ms;
7 --animation-1500: 1500ms;
8}
9
10body {
11 font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
12 sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
13 color: white;
14 background-color: var(--color-primary);
15 min-height: 100vh;
16 width: 100%;
17 padding: 0;
18 margin: 0;
19 overflow-x: hidden;
20 text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;
21 text-rendering: optimizeLegibility;
22 user-select: none;
23}
24
25/* The custom cursor replaces the real one, so hiding the real one is only safe
26 where the replacement exists. It is display:none below 1024px (see
27 components/cursor.css), and it is drawn by a deferred module, so an
28 unconditional rule left a real-pointer visitor with no pointer at all between
29 first paint and hydration, and permanently in any window narrower than
30 1024px: a half-screen desktop window, a 1366px laptop at 125% scaling, an
31 iPad with a trackpad. It also discarded every OS pointer accessibility
32 setting for anyone relying on one. */
33@media (min-width: 1024px) and (pointer: fine) {
34 html[data-js] body,
35 html[data-js] a,
36 html[data-js] button {
37 cursor: none;
38 }
39}
40
41/* The Next.js version cross-faded two overlapping page trees with
42 react-transition-group, which is why the old rule was position: absolute:
43 during a route change both the outgoing and incoming page were mounted at
44 once and had to occupy the same space.
45
46 Real navigation only ever has one document, so this is a plain fade-in on
47 load in normal flow. Same effect to the eye, and it fixes a side effect of
48 the old approach: an absolutely positioned wrapper contributes nothing to
49 body height, so the page scrolled only because overflow happened to extend
50 the scroll area.
51
52 position: relative is required here. The full bleed white panels on about,
53 code and art are position: absolute with top/bottom 0, so they size to the
54 nearest positioned ancestor. With nothing positioned between them and the
55 root they fall back to the initial containing block, which is the viewport,
56 so the white stops at 100vh and the dark body colour shows through on
57 scroll. */
58.transition {
59 position: relative;
60 min-height: 100vh;
61 width: 100%;
62 animation: pageFadeIn var(--transition-250) ease-in-out both;
63}
64
65@keyframes pageFadeIn {
66 from {
67 opacity: 0;
68 }
69 to {
70 opacity: 1;
71 }
72}
73
74@keyframes pageFadeOut {
75 from {
76 opacity: 1;
77 }
78 to {
79 opacity: 0;
80 }
81}
82
83/* Page to page: fade out to white, then fade in.
84
85 The opening curtain now plays once per visit rather than on every navigation
86 (see loader.css), so everything after it needs a quieter transition of its
87 own. This is a cross document view transition, which means the browser
88 drives it: it holds the outgoing page on screen until the incoming one is
89 ready to paint. That is the reason to use it over the obvious hand rolled
90 version, where JS fades to white, waits a fixed delay and then sets
91 location. That version bets on a fixed guess about latency and loses the bet
92 in both directions, either cutting the fade short or parking the visitor on
93 a blank white screen. Here there is no guess and no added delay.
94
95 Where it is unsupported nothing below applies, navigation is instant, and
96 the pageFadeIn above is still the whole effect. That is an acceptable
97 floor: a gentle fade in with no fade out. */
98@view-transition {
99 navigation: auto;
100}
101
102/* The transition overlay is painted above the real document for its whole
103 duration, so the white has to live here. Anything set on the page
104 underneath would be covered up by the very thing we want it to show
105 through. */
106::view-transition {
107 background-color: white;
108}
109
110/* The default UA style cross fades the two snapshots with plus-lighter, which
111 is correct for an overlapping fade and wrong for a sequential one. These
112 never overlap: the old is gone before the new starts. */
113::view-transition-old(root) {
114 mix-blend-mode: normal;
115 animation: pageFadeOut calc(var(--transition-250) / 2) ease-in both;
116}
117
118::view-transition-new(root) {
119 mix-blend-mode: normal;
120 animation: pageFadeIn calc(var(--transition-250) / 2)
121 calc(var(--transition-250) / 2) ease-out both;
122}
123
124@media (prefers-reduced-motion: reduce) {
125 .transition {
126 animation: none;
127 }
128}
129
130/* Opting out at the source rather than zeroing the animations. Same result to
131 look at, but the browser skips capturing the snapshots at all instead of
132 capturing them and then playing nothing. */
133@media (prefers-reduced-motion: reduce) {
134 @view-transition {
135 navigation: none;
136 }
137}