repos
/ orchard main

orchard

mirror

Every 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

9.6 KB · 479 lines · CSS Raw History
  1@keyframes contact-transformRight {
  2  from {
  3    transform: scaleX(0);
  4  }
  5  to {
  6    transform: scaleX(1);
  7  }
  8}
  9
 10@keyframes contact-transformLeft {
 11  from {
 12    transform: scaleX(1);
 13  }
 14  to {
 15    transform: scaleX(0);
 16  }
 17}
 18
 19/* The two halves arrive from opposite edges and meet at the seam, rather than
 20   the whole grid sliding in from the left as one sheet. Off screen start is
 21   -120% on the left panel and not -100%: the grid is inset by the 60px
 22   sidebar, so an exact -100% would park its right edge at x=60 and leave a
 23   dark sliver showing behind the sidebar for the length of the animation. */
 24@keyframes contact-panelInLeft {
 25  from {
 26    transform: translateX(-120%);
 27  }
 28  to {
 29    transform: translateX(0);
 30  }
 31}
 32
 33@keyframes contact-panelInRight {
 34  from {
 35    transform: translateX(100%);
 36  }
 37  to {
 38    transform: translateX(0);
 39  }
 40}
 41
 42/* Everything below is the content stagger inside the dark panel, which only
 43   starts once the panels have landed. The delays are absolute rather than
 44   relative so the whole sequence reads as one timeline from page load. */
 45@keyframes contact-ruleDraw {
 46  from {
 47    transform: scaleX(0);
 48  }
 49  to {
 50    transform: scaleX(1);
 51  }
 52}
 53
 54@keyframes contact-riseIn {
 55  from {
 56    opacity: 0;
 57    transform: translateY(12px);
 58  }
 59  to {
 60    opacity: 1;
 61    transform: translateY(0);
 62  }
 63}
 64
 65@keyframes contact-barIn {
 66  from {
 67    opacity: 0;
 68    transform: translateX(-28px);
 69  }
 70  to {
 71    opacity: 1;
 72    transform: translateX(0);
 73  }
 74}
 75
 76.contact-background {
 77  position: fixed;
 78  top: 0;
 79  left: 0;
 80  right: 0;
 81  bottom: 0;
 82  background-color: white;
 83  z-index: -2;
 84}
 85
 86.contact-grid {
 87  display: grid;
 88  /* Lands the seam on the centre alignment line rather than 60px past it. The
 89     line grid is 60px 15% 1fr 1fr 15% 60px, which is symmetric, so its middle
 90     border sits at exactly 50vw at any width. This grid is inset by the 60px
 91     sidebar, so a plain 50vw column ends at 60px + 50vw and overshoots by the
 92     width of that inset. */
 93  grid-template-columns: calc(50vw - 60px) 1fr;
 94  grid-template-rows: auto;
 95  grid-template-areas: "left right";
 96  margin-left: 60px;
 97  width: calc(100vw - 60px);
 98}
 99
100@media (max-width: 1023.98px) {
101  .contact-grid {
102    margin-left: 0;
103    width: 100%;
104    grid-template-columns: 1fr;
105    grid-template-rows: auto auto;
106    grid-template-areas:
107      "left"
108      "right";
109    margin-top: 60px;
110  }
111}
112
113.contact-gridLeft {
114  grid-area: left;
115  animation: contact-panelInLeft 550ms 100ms cubic-bezier(0.16, 1, 0.3, 1) both;
116  background-color: rgba(0, 0, 0, 0.9);
117  min-height: 100vh;
118  display: flex;
119  align-items: center;
120}
121
122@media (max-width: 1023.98px) {
123  .contact-gridLeft {
124    grid-column: 1;
125    grid-row: 1;
126    min-height: auto;
127    align-items: flex-start;
128  }
129}
130
131.contact-gridRight {
132  grid-area: right;
133  /* Nothing marks this panel's leading edge. A gradient seam and then a soft
134     shadow were both tried here and both rejected as looking wrong, so the
135     white simply arrives over white: the entrance that reads is the dark
136     column plus the staggered content, and the grid lines below stay put
137     through all of it. */
138  animation: contact-panelInRight 550ms 100ms cubic-bezier(0.16, 1, 0.3, 1) both;
139  height: 100vh;
140  width: 100%;
141  display: flex;
142  align-items: flex-end;
143  overflow-y: auto;
144  background-color: white;
145}
146
147@media (max-width: 1023.98px) {
148  .contact-gridRight {
149    grid-column: 1;
150    grid-row: 2;
151    height: auto;
152    min-height: 50vh;
153    overflow-y: visible;
154    align-items: flex-start;
155    padding-top: 20px;
156    padding-bottom: 30px;
157  }
158}
159
160.contact-contactWrapper {
161  padding-left: 60px;
162}
163
164@media (max-width: 1023.98px) {
165  .contact-contactWrapper {
166    min-height: auto;
167    padding: 10px 20px 30px;
168  }
169}
170
171.contact-heading {
172  font-size: 3em;
173  margin-top: 10px;
174  margin-bottom: 20px;
175  animation: contact-riseIn 400ms 650ms ease-out both;
176}
177
178.contact-heading::before {
179  content: "";
180  display: block;
181  width: 60px;
182  height: 4px;
183  margin-bottom: 10px;
184  background-image: linear-gradient(
185    to right,
186    var(--color-blue) 0,
187    var(--color-purple) 100%
188  );
189  transform-origin: left;
190  animation: contact-ruleDraw 400ms 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
191}
192
193@media (max-width: 767.98px) {
194  .contact-heading {
195    margin-top: 0;
196    margin-bottom: 10px;
197  }
198}
199
200.contact-shoutOut {
201  text-transform: uppercase;
202  animation: contact-barIn 400ms cubic-bezier(0.16, 1, 0.3, 1) both;
203  padding: 5px;
204  margin: 0 0 2px 0;
205  float: left;
206  clear: left;
207  font-size: 1.4em;
208  background-image: linear-gradient(
209    to right,
210    var(--color-blue) 0,
211    var(--color-purple) 100%
212  );
213}
214
215@media (max-width: 767.98px) {
216  .contact-shoutOut {
217    font-size: 1.1em;
218  }
219}
220
221/* The bars and the contact pairs are staggered with nth-of-type and
222   nth-child rather than a per element delay written into the template,
223   because the markup is a plain h2 pair and a dl. The dl buckets are per
224   pair (dt then dd share a delay) and the last bucket is open ended, so
225   adding a fifth contact method degrades to landing with the fourth
226   instead of not animating at all. */
227.contact-shoutOut:nth-of-type(1) {
228  animation-delay: 800ms;
229}
230
231.contact-shoutOut:nth-of-type(2) {
232  animation-delay: 880ms;
233}
234
235.contact-contactList {
236  padding-top: 30px;
237  clear: both;
238}
239
240.contact-contactList dt,
241.contact-contactList dd {
242  animation: contact-riseIn 400ms ease-out both;
243}
244
245.contact-contactList > :nth-child(-n + 2) {
246  animation-delay: 960ms;
247}
248
249.contact-contactList > :nth-child(n + 3):nth-child(-n + 4) {
250  animation-delay: 1030ms;
251}
252
253.contact-contactList > :nth-child(n + 5):nth-child(-n + 6) {
254  animation-delay: 1100ms;
255}
256
257.contact-contactList > :nth-child(n + 7) {
258  animation-delay: 1170ms;
259}
260
261.contact-contactKey {
262  text-transform: uppercase;
263  font-size: 0.75em;
264  font-weight: 700;
265  color: rgba(255, 255, 255, 0.4);
266  letter-spacing: 2px;
267  font-family: monospace;
268}
269
270.contact-contactValue {
271  font-size: 1.5em;
272  margin-left: 0;
273  margin-bottom: 30px;
274}
275
276.contact-contactLink {
277  color: white;
278  text-decoration: none;
279  position: relative;
280}
281
282.contact-contactLink::before {
283  content: "";
284  position: absolute;
285  left: 0;
286  right: 0;
287  bottom: 0;
288  height: 2px;
289  background: rgba(255, 255, 255, 1);
290  transform-origin: left;
291  animation: contact-transformLeft 300ms normal forwards;
292}
293
294.contact-contactLink::after {
295  content: "";
296  position: absolute;
297  left: 0;
298  right: 0;
299  bottom: 0;
300  height: 2px;
301  background: rgba(255, 255, 255, 0.2);
302}
303
304.contact-contactLink:hover::before {
305  animation: contact-transformRight 300ms normal forwards;
306}
307
308.contact-chat {
309  display: flex;
310  flex-direction: column;
311  justify-content: flex-end;
312  padding: 30px;
313  width: 100%;
314  min-height: 100%;
315}
316
317@media (max-width: 1023.98px) {
318  .contact-chat {
319    justify-content: flex-start;
320  }
321}
322
323.contact-chatLine {
324  display: flex;
325  margin-bottom: 12px;
326  align-items: flex-start;
327  animation: contact-riseIn 300ms ease-out both;
328}
329
330.contact-chatLineUser {
331  justify-content: flex-end;
332}
333
334.contact-chatBubble {
335  font-size: 1.1em;
336  padding: 12px 18px;
337  background: rgba(0, 0, 0, 0.06);
338  border-radius: var(--radius-2);
339  color: black;
340  max-width: 75%;
341  line-height: 1.5;
342}
343
344@media (max-width: 767.98px) {
345  .contact-chatBubble {
346    font-size: 1em;
347  }
348}
349
350.contact-chatBubble span {
351  display: block;
352  font-size: 0.65em;
353  text-transform: uppercase;
354  margin-top: 8px;
355  color: var(--color-blue);
356  letter-spacing: 2px;
357  font-weight: 700;
358  font-family: monospace;
359}
360
361.contact-chatAvatar {
362  margin-right: 16px;
363  width: 40px;
364  height: 40px;
365  display: inline-flex;
366  flex-shrink: 0;
367}
368
369@media (max-width: 767.98px) {
370  .contact-chatAvatar {
371    margin-right: 10px;
372    width: 36px;
373    height: 36px;
374  }
375}
376
377.contact-chatBubbleUser {
378  background-image: linear-gradient(
379    to right,
380    var(--color-blue) 0,
381    var(--color-purple) 100%
382  );
383  color: white;
384}
385
386.contact-chatBubbleUser span {
387  display: none;
388}
389
390.contact-chatOptions {
391  display: flex;
392  flex-wrap: wrap;
393  gap: 8px;
394  animation: contact-riseIn 300ms ease-out both;
395  margin-top: 4px;
396  margin-bottom: 12px;
397  padding-left: 56px;
398}
399
400@media (max-width: 767.98px) {
401  .contact-chatOptions {
402    padding-left: 0;
403  }
404}
405
406.contact-chatOption {
407  padding: 8px 16px;
408  font-size: 0.9em;
409  font-weight: 600;
410  background: none;
411  border: 1px solid rgba(0, 0, 0, 0.2);
412  border-radius: var(--radius-2);
413  color: black;
414  cursor: none;
415  transition: border-color 200ms, background 200ms;
416  font-family: inherit;
417}
418
419.contact-chatOption:hover {
420  border-color: var(--color-blue);
421  background: rgba(14, 63, 244, 0.05);
422}
423
424@keyframes contact-dotPulse {
425  0%, 80%, 100% {
426    opacity: 0.3;
427  }
428  40% {
429    opacity: 1;
430  }
431}
432
433.contact-typingDots {
434  display: inline-flex;
435  gap: 4px;
436  align-items: center;
437}
438
439.contact-typingDots span {
440  display: inline-block;
441  width: 6px;
442  height: 6px;
443  background: rgba(0, 0, 0, 0.4);
444  border-radius: 50%;
445  animation: contact-dotPulse 1.4s infinite;
446  margin-top: 0;
447  font-size: inherit;
448  letter-spacing: normal;
449  text-transform: none;
450  font-weight: normal;
451  font-family: inherit;
452  color: transparent;
453}
454
455.contact-typingDots span:nth-child(2) {
456  animation-delay: 0.2s;
457}
458
459.contact-typingDots span:nth-child(3) {
460  animation-delay: 0.4s;
461}
462
463/* Reduced motion keeps the page and drops the choreography: panels are simply
464   in place, content is simply visible. The hover underline and the typing dots
465   stay, since they are feedback rather than entrance decoration. */
466@media (prefers-reduced-motion: reduce) {
467  .contact-gridLeft,
468  .contact-gridRight,
469  .contact-heading,
470  .contact-heading::before,
471  .contact-shoutOut,
472  .contact-contactList dt,
473  .contact-contactList dd,
474  .contact-chatLine,
475  .contact-chatOptions {
476    animation: none;
477  }
478}