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 code-transformRight {
2 from {
3 transform: scaleX(0);
4 }
5 to {
6 transform: scaleX(1);
7 }
8}
9
10@keyframes code-transformLeft {
11 from {
12 transform: scaleX(1);
13 }
14 to {
15 transform: scaleX(0);
16 }
17}
18
19@keyframes code-cardEnter {
20 from {
21 opacity: 0;
22 transform: translateY(20px);
23 }
24 to {
25 opacity: 1;
26 transform: translateY(0);
27 }
28}
29
30.code-background {
31 position: absolute;
32 top: 0;
33 left: 0;
34 right: 0;
35 bottom: 0;
36 background-color: white;
37 z-index: -2;
38}
39
40.code-heading {
41 font-weight: 700;
42 margin-top: 60px;
43 margin-bottom: 20px;
44 font-size: 2.5em;
45 color: black;
46}
47
48.code-heading::before {
49 content: "";
50 display: block;
51 width: 50px;
52 height: 5px;
53 margin-bottom: 20px;
54 background-color: var(--color-blue);
55}
56
57.code-paragraph {
58 font-size: 1.5em;
59 margin-top: 0;
60 margin-bottom: 30px;
61 font-weight: 300;
62 color: black;
63}
64
65.code-paragraph a {
66 color: black;
67 text-decoration: none;
68 position: relative;
69 white-space: nowrap;
70}
71
72.code-paragraph a::before {
73 content: "";
74 position: absolute;
75 left: 0;
76 right: 0;
77 bottom: 0;
78 height: 2px;
79 background: rgba(0, 0, 0, 1);
80 transform-origin: left;
81 animation: code-transformLeft 300ms normal forwards;
82}
83
84.code-paragraph a::after {
85 content: "";
86 position: absolute;
87 left: 0;
88 right: 0;
89 bottom: 0;
90 height: 2px;
91 background: rgba(0, 0, 0, 0.2);
92}
93
94.code-paragraph a:hover::before {
95 animation: code-transformRight 300ms normal forwards;
96}
97
98.code-projects {
99 display: flex;
100 width: 100%;
101 flex-wrap: wrap;
102 gap: 20px;
103}
104
105/* Only the last grid on the page needs room under it. When a section follows,
106 that section's own heading margin is the gap, and adding this as well left a
107 void under a short list. */
108.code-projects:last-child {
109 padding-bottom: 60px;
110}
111
112.code-project {
113 width: calc(50% - 10px);
114 display: flex;
115 flex-direction: column;
116 animation: code-cardEnter 600ms cubic-bezier(0.16, 1, 0.3, 1) both;
117}
118
119@media (max-width: 767.98px) {
120 .code-project {
121 width: 100%;
122 }
123}
124
125.code-projectHeading {
126 font-weight: 700;
127 margin-top: 40px;
128 margin-bottom: 8px;
129 font-size: 2em;
130 color: black;
131}
132
133.code-tags {
134 display: flex;
135 flex-wrap: wrap;
136 gap: 6px;
137 margin-bottom: 12px;
138}
139
140.code-tag {
141 display: inline-block;
142 font-size: 0.75em;
143 padding: 2px 8px;
144 background-color: var(--color-blue);
145 border-radius: var(--radius-2);
146 color: white;
147 font-family: monospace;
148 text-transform: uppercase;
149 letter-spacing: 1px;
150}
151
152.code-projectParagraph {
153 font-size: 1.3em;
154 margin-top: 0;
155 margin-bottom: 16px;
156 font-weight: 300;
157 color: black;
158 flex-grow: 1;
159 line-height: 1.5;
160}
161
162.code-projectCommit {
163 font-family: monospace;
164 background: var(--color-primary);
165 color: #00ff00;
166 padding: 16px;
167 overflow-x: hidden;
168 max-width: 100%;
169 text-overflow: ellipsis;
170 font-size: 0.85em;
171 line-height: 1.6;
172 border-radius: var(--radius-2);
173 margin-top: 0;
174 margin-bottom: 16px;
175}
176
177.code-projectButton {
178 padding: 8px 12px;
179 font-size: 0.9em;
180 text-decoration: none;
181 background-color: black;
182 font-weight: 700;
183 text-transform: uppercase;
184 display: inline-flex;
185 align-items: center;
186 justify-content: center;
187 letter-spacing: 2px;
188 color: white;
189 background-image: linear-gradient(
190 to right,
191 var(--color-blue) 0,
192 var(--color-purple) 100%
193 );
194 transform: scale(1);
195 transition-duration: 250ms;
196 transition-property: transform;
197 width: 120px;
198 text-align: center;
199 border-radius: var(--radius-2);
200}
201
202.code-projectButton svg {
203 margin-right: 10px;
204}
205
206.code-projectButton:hover {
207 transform: scale(1.2);
208}
209
210/* The live cards carry two buttons instead of one. The gap is wider than it
211 looks like it needs to be because the buttons scale to 1.2 on hover and a
212 tighter gap put one on top of its neighbour. */
213.code-projectButtons {
214 display: flex;
215 flex-wrap: wrap;
216 gap: 16px;
217}
218
219.code-project-live .code-projectButton {
220 width: auto;
221 min-width: 120px;
222 padding-left: 16px;
223 padding-right: 16px;
224}
225
226.code-projectButton-secondary {
227 background-image: none;
228 background-color: transparent;
229 color: black;
230 border: 2px solid rgba(0, 0, 0, 0.2);
231 transition-property: transform, border-color;
232}
233
234.code-projectButton-secondary:hover {
235 border-color: var(--color-blue);
236}
237
238.code-sectionNote-live {
239 color: black;
240}
241
242/* The active / archived split.
243 *
244 * The archived half is quieter: grey tag pills, an outline button instead of a
245 * gradient, softer body text. A separate palette rather than a filter, because
246 * grayscale() would also flatten the heading and the focus ring.
247 *
248 * Hovering one brings the colour back. */
249
250.code-sectionHead {
251 display: flex;
252 align-items: center;
253 gap: 16px;
254 width: 100%;
255 margin-top: 50px;
256}
257
258.code-sectionTitle {
259 font-weight: 700;
260 font-size: 1.1em;
261 text-transform: uppercase;
262 letter-spacing: 3px;
263 color: black;
264 margin: 0;
265}
266
267/* The rule takes the leftover width, so the count always sits at the far edge
268 however long the title is. */
269.code-sectionRule {
270 flex: 1;
271 height: 2px;
272 background-color: var(--color-blue);
273}
274
275.code-sectionCount {
276 font-family: monospace;
277 font-size: 0.85em;
278 letter-spacing: 2px;
279 color: var(--color-blue);
280}
281
282.code-sectionHead-archived .code-sectionRule {
283 background-color: rgba(0, 0, 0, 0.15);
284}
285
286.code-sectionHead-archived .code-sectionCount {
287 color: rgba(0, 0, 0, 0.4);
288}
289
290.code-sectionHead-archived .code-sectionTitle {
291 color: rgba(0, 0, 0, 0.55);
292}
293
294.code-sectionNote {
295 font-size: 1.1em;
296 font-weight: 300;
297 color: rgba(0, 0, 0, 0.55);
298 margin: 12px 0 0;
299 max-width: 46em;
300}
301
302/* The heading margin already spaces the first row of cards. */
303.code-projects-archived .code-projectHeading {
304 margin-top: 30px;
305 color: rgba(0, 0, 0, 0.65);
306}
307
308.code-project-archived .code-projectParagraph {
309 color: rgba(0, 0, 0, 0.55);
310}
311
312.code-project-archived .code-tag {
313 background-color: rgba(0, 0, 0, 0.35);
314}
315
316/* The one pill that stays legible at rest: it is the label doing the work of
317 the whole section. */
318.code-tag-archived {
319 background-color: transparent !important;
320 color: rgba(0, 0, 0, 0.55);
321 border: 1px solid rgba(0, 0, 0, 0.25);
322 padding: 1px 7px;
323}
324
325.code-project-archived .code-projectButton {
326 background-image: none;
327 background-color: transparent;
328 color: rgba(0, 0, 0, 0.6);
329 border: 2px solid rgba(0, 0, 0, 0.2);
330 transition-property: transform, color, border-color, background-color;
331}
332
333.code-project-archived:hover .code-projectHeading,
334.code-project-archived:focus-within .code-projectHeading {
335 color: black;
336}
337
338.code-project-archived:hover .code-projectParagraph,
339.code-project-archived:focus-within .code-projectParagraph {
340 color: black;
341}
342
343.code-project-archived:hover .code-tag,
344.code-project-archived:focus-within .code-tag {
345 background-color: var(--color-blue);
346}
347
348.code-project-archived:hover .code-projectButton,
349.code-project-archived:focus-within .code-projectButton {
350 background-image: linear-gradient(
351 to right,
352 var(--color-blue) 0,
353 var(--color-purple) 100%
354 );
355 border-color: transparent;
356 color: white;
357}
358
359/* The card already animates in; anything that only fires on hover is decoration
360 and should stop when asked. */
361@media (prefers-reduced-motion: reduce) {
362 .code-project,
363 .code-project-archived .code-projectButton {
364 animation: none;
365 transition: none;
366 }
367}