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@keyframes art-transformRight {
2 from {
3 transform: scaleX(0);
4 }
5 to {
6 transform: scaleX(1);
7 }
8}
9
10@keyframes art-transformLeft {
11 from {
12 transform: scaleX(1);
13 }
14 to {
15 transform: scaleX(0);
16 }
17}
18
19@keyframes art-lightboxFadeIn {
20 from {
21 opacity: 0;
22 }
23 to {
24 opacity: 1;
25 }
26}
27
28@keyframes art-lightboxScaleIn {
29 from {
30 opacity: 0;
31 transform: scale(0.92);
32 }
33 to {
34 opacity: 1;
35 transform: scale(1);
36 }
37}
38
39@keyframes art-lightboxPulse {
40 0%, 100% {
41 opacity: 0.4;
42 }
43 50% {
44 opacity: 1;
45 }
46}
47
48.art-background {
49 position: absolute;
50 top: 0;
51 left: 0;
52 right: 0;
53 bottom: 0;
54 background-color: white;
55 z-index: -2;
56}
57
58.art-heading {
59 font-weight: 700;
60 margin-top: 60px;
61 margin-bottom: 10px;
62 font-size: 2.5em;
63 color: black;
64}
65
66.art-heading::before {
67 content: "";
68 display: block;
69 width: 50px;
70 height: 5px;
71 margin-bottom: 20px;
72 background-image: linear-gradient(
73 to right,
74 var(--color-blue) 0,
75 var(--color-purple) 100%
76 );
77}
78
79.art-paragraph {
80 font-size: 1.3em;
81 margin-top: 0;
82 margin-bottom: 24px;
83 font-weight: 300;
84 color: rgba(0, 0, 0, 0.7);
85 line-height: 1.6;
86}
87
88.art-subheading {
89 font-weight: 700;
90 margin-top: 48px;
91 margin-bottom: 12px;
92 font-size: 1.6em;
93 color: black;
94 display: flex;
95 align-items: center;
96 gap: 12px;
97}
98
99.art-subheading > span {
100 font-family: monospace;
101 font-size: 0.7em;
102 padding: 2px 8px;
103 background-image: linear-gradient(
104 to right,
105 var(--color-blue) 0,
106 var(--color-purple) 100%
107 );
108 color: white;
109 border-radius: var(--radius-2);
110 letter-spacing: 1px;
111}
112
113.art-paragraph a {
114 color: black;
115 text-decoration: none;
116 position: relative;
117 white-space: nowrap;
118}
119
120.art-paragraph a::before {
121 content: "";
122 position: absolute;
123 left: 0;
124 right: 0;
125 bottom: 0;
126 height: 2px;
127 background: rgba(0, 0, 0, 1);
128 transform-origin: left;
129 animation: art-transformLeft 300ms normal forwards;
130}
131
132.art-paragraph a::after {
133 content: "";
134 position: absolute;
135 left: 0;
136 right: 0;
137 bottom: 0;
138 height: 2px;
139 background: rgba(0, 0, 0, 0.2);
140}
141
142.art-paragraph a:hover::before {
143 animation: art-transformRight 300ms normal forwards;
144}
145
146.art-artGrid {
147 display: grid;
148 grid-template-columns: repeat(2, 1fr);
149 gap: 16px;
150}
151
152@media (max-width: 767.98px) {
153 .art-artGrid {
154 grid-template-columns: 1fr;
155 }
156}
157
158.art-artItem {
159 position: relative;
160 overflow: hidden;
161 border-radius: var(--radius-2);
162 transition: transform 300ms, box-shadow 300ms;
163}
164
165.art-artItem:hover {
166 transform: translateY(-3px);
167 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
168}
169
170.art-cardImage {
171 display: block;
172 background-color: var(--color-primary);
173 aspect-ratio: 16 / 9;
174 overflow: hidden;
175}
176
177.art-cardImage img {
178 object-fit: cover;
179 object-position: center;
180 width: 100%;
181 height: auto;
182 aspect-ratio: 16 / 9;
183 display: block;
184 transition: opacity 400ms ease-out;
185}
186
187/* Gated on data-js for the same reason as .index-hero: initCardFades reveals
188 these, so ungated a JS failure renders seven blank boxes where the artwork is. */
189html[data-js] .art-cardImage img {
190 opacity: 0;
191}
192
193/* Matches the specificity of the data-js gate above, which would otherwise
194 outrank this and leave every card stuck at opacity 0. */
195html[data-js] .art-cardImage img.art-imgLoaded {
196 opacity: 1;
197}
198
199.art-artItemHeader {
200 margin: 0;
201 padding: 10px 14px;
202 display: flex;
203 justify-content: space-between;
204 align-items: center;
205 font-weight: 700;
206 font-size: 0.95em;
207 background: black;
208 color: white;
209}
210
211.art-artItemHeader > span {
212 font-family: monospace;
213 font-weight: 400;
214 font-size: 1em;
215 color: rgba(255, 255, 255, 0.8);
216 letter-spacing: 1px;
217}
218
219.art-artItemButton {
220 padding: 8px 14px;
221 font-size: 0.9em;
222 text-decoration: none;
223 font-weight: 700;
224 text-transform: uppercase;
225 display: inline-block;
226 letter-spacing: 2px;
227 color: white;
228 background-image: linear-gradient(
229 to right,
230 var(--color-blue) 0,
231 var(--color-purple) 100%
232 );
233 border-radius: var(--radius-2);
234 transform: scale(1);
235 transition: transform 250ms;
236 margin-bottom: 20px;
237}
238
239.art-artItemButton:hover {
240 transform: scale(1.1);
241}
242
243.art-artContainer {
244 width: 100%;
245 height: 500px;
246 max-height: 100vh;
247 background: black;
248 margin-bottom: 16px;
249 position: relative;
250 overflow: hidden;
251 border-radius: var(--radius-2);
252}
253
254.art-playButton {
255 position: absolute;
256 top: 16px;
257 right: 16px;
258 width: 44px;
259 height: 44px;
260 border: 1px solid rgba(255, 255, 255, 0.2);
261 border-radius: var(--radius-2);
262 background: rgba(0, 0, 0, 0.5);
263 color: white;
264 font-size: 20px;
265 line-height: 1;
266 padding: 0;
267 cursor: pointer;
268 display: flex;
269 align-items: center;
270 justify-content: center;
271 transition: border-color 200ms, background 200ms;
272 z-index: 5;
273}
274
275.art-playButton:hover {
276 border-color: var(--color-blue);
277 background: rgba(14, 63, 244, 0.2);
278}
279
280.art-playButton:active {
281 transform: scale(0.95);
282}
283
284/* Lightbox */
285
286/* The hidden attribute sets display:none from the UA stylesheet, which any
287 author display rule outranks. Without this the lightbox is open on arrival.
288 The React version had no equivalent because it rendered the portal only
289 while an image was selected, so there was nothing to hide. */
290.art-lightboxOverlay[hidden] {
291 display: none;
292}
293
294.art-lightboxOverlay {
295 position: fixed;
296 top: 0;
297 left: 0;
298 right: 0;
299 bottom: 0;
300 background: rgba(0, 0, 0, 0.92);
301 display: flex;
302 align-items: center;
303 justify-content: center;
304 z-index: 99999;
305 animation: art-lightboxFadeIn 300ms ease-out;
306}
307
308.art-lightboxImageWrapper {
309 position: relative;
310 width: 90vw;
311 height: 90vh;
312 animation: art-lightboxScaleIn 400ms cubic-bezier(0.16, 1, 0.3, 1);
313 background: black;
314 border-radius: var(--radius-2);
315 overflow: hidden;
316}
317
318.art-lightboxImage {
319 /* next/image fill mode supplied the absolute positioning inline. */
320 position: absolute;
321 inset: 0;
322 width: 100%;
323 height: 100%;
324 object-fit: contain;
325 visibility: hidden;
326 opacity: 0;
327 transition: opacity 300ms ease-out;
328}
329
330.art-lightboxLoading {
331 position: absolute;
332 top: 50%;
333 left: 50%;
334 transform: translate(-50%, -50%);
335 color: var(--color-blue);
336 font-family: monospace;
337 font-size: 1.2em;
338 letter-spacing: 4px;
339 text-transform: uppercase;
340 animation: art-lightboxPulse 1.5s ease-in-out infinite;
341}
342
343.art-show {
344 opacity: 1;
345 visibility: visible;
346}
347
348.art-hide {
349 opacity: 0;
350 visibility: hidden;
351}
352
353.art-lightboxClose {
354 position: absolute;
355 top: 16px;
356 right: 16px;
357 width: 40px;
358 height: 40px;
359 background: rgba(0, 0, 0, 0.6);
360 border: 1px solid rgba(255, 255, 255, 0.2);
361 border-radius: var(--radius-2);
362 color: white;
363 font-size: 1.2em;
364 display: flex;
365 align-items: center;
366 justify-content: center;
367 transition: border-color 200ms, background 200ms;
368 z-index: 1;
369}
370
371.art-lightboxClose:hover {
372 border-color: var(--color-blue);
373 background: rgba(14, 63, 244, 0.2);
374}