Skip to content

g1t/apps/web/app/app.css

604 lines12,934 bytesCodeBlame
1@import "tailwindcss" source(".");
2@import "@g1t/theme/tokens.css";
3@import "@g1t/theme/fonts.css";
4
5/* Tailwind's names for the shared g1t tokens. */
6@theme {
7 --font-sans: var(--g1t-font-sans);
8 --font-display: var(--g1t-font-display);
9 --font-mono: var(--g1t-font-mono);
10
11 /* Hanken and Bricolage want less tightening than a text face set close. */
12 --tracking-tight: -0.012em;
13
14 --color-bg: var(--g1t-bg);
15 --color-surface: var(--g1t-surface);
16 --color-raised: var(--g1t-raised);
17 --color-line: var(--g1t-line);
18 --color-line-strong: var(--g1t-line-strong);
19
20 --color-fg: var(--g1t-fg);
21 --color-fg-soft: var(--g1t-fg-soft);
22 --color-muted: var(--g1t-muted);
23 --color-faint: var(--g1t-faint);
24
25 --color-accent: var(--g1t-accent);
26 --color-accent-dim: var(--g1t-accent-dim);
27
28 --color-info: var(--g1t-info);
29 --color-merged: var(--g1t-merged);
30 --color-warn: var(--g1t-warn);
31 --color-danger: var(--g1t-danger);
32
33 --animate-fade-up: fade-up 0.6s ease-out both;
34 --animate-dash: dash 1.2s linear infinite;
35
36 /* Menus, popovers and dialogs (components/ui): in quickly, out quicker. */
37 --animate-pop-in: pop-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
38 --animate-pop-out: pop-out 0.1s ease-in forwards;
39 --animate-fade-in: fade-in 0.16s ease-out;
40 --animate-fade-out: fade-out 0.12s ease-in forwards;
41 --animate-dialog-in: dialog-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
42 --animate-dialog-out: dialog-out 0.12s ease-in forwards;
43 /* Sheets (components/ui/sheet.tsx) slide in from their edge. */
44 --animate-sheet-in-right: sheet-in-right 0.24s cubic-bezier(0.16, 1, 0.3, 1);
45 --animate-sheet-out-right: sheet-out-right 0.16s ease-in forwards;
46 --animate-sheet-in-left: sheet-in-left 0.24s cubic-bezier(0.16, 1, 0.3, 1);
47 --animate-sheet-out-left: sheet-out-left 0.16s ease-in forwards;
48}
49
50@keyframes sheet-in-right {
51 from {
52 transform: translateX(100%);
53 }
54}
55
56@keyframes sheet-out-right {
57 to {
58 transform: translateX(100%);
59 }
60}
61
62@keyframes sheet-in-left {
63 from {
64 transform: translateX(-100%);
65 }
66}
67
68@keyframes sheet-out-left {
69 to {
70 transform: translateX(-100%);
71 }
72}
73
74@keyframes pop-in {
75 from {
76 opacity: 0;
77 transform: scale(0.96) translateY(-2px);
78 }
79}
80
81@keyframes pop-out {
82 to {
83 opacity: 0;
84 transform: scale(0.96);
85 }
86}
87
88@keyframes fade-in {
89 from {
90 opacity: 0;
91 }
92}
93
94@keyframes fade-out {
95 to {
96 opacity: 0;
97 }
98}
99
100@keyframes dialog-in {
101 from {
102 opacity: 0;
103 transform: translateY(6px) scale(0.97);
104 }
105}
106
107@keyframes dialog-out {
108 to {
109 opacity: 0;
110 transform: translateY(6px) scale(0.97);
111 }
112}
113
114/* Never from nothing: the first paint already shows the words. */
115@keyframes fade-up {
116 from {
117 opacity: 0.4;
118 transform: translateY(8px);
119 }
120}
121
122@keyframes dash {
123 to {
124 stroke-dashoffset: -16;
125 }
126}
127
128html {
129 color-scheme: dark;
130 scroll-behavior: smooth;
131}
132
133body {
134 @apply bg-bg font-sans text-fg antialiased;
135}
136
137::selection {
138 background: color-mix(in srgb, var(--color-accent) 35%, transparent);
139}
140
141/* In the base layer, so a component can opt out with a utility. */
142@layer base {
143 :focus-visible {
144 outline: 2px solid var(--color-accent);
145 outline-offset: 2px;
146 }
147
148 /*
149 * A grid's items may shrink below their content. Without this, one long
150 * line that does not wrap (a command, a commit hash, an address) widens
151 * the whole column, and on a phone the page scrolls sideways.
152 */
153 .grid > * {
154 min-width: 0;
155 }
156
157 /*
158 * Headlines are set in Bricolage Grotesque, its optical size following
159 * the font size; small section labels that happen to be headings stay in
160 * the text face. A utility on the element still wins.
161 */
162 :where(h1, h2),
163 :where(h3):is(.text-xl, .text-2xl, .text-3xl, .text-4xl) {
164 font-family: var(--font-display);
165 font-optical-sizing: auto;
166 }
167 :where(h2):is(.text-xs, .text-sm, .text-base) {
168 font-family: var(--font-sans);
169 }
170
171 /* Checkboxes, radios and ranges in g1t's own colour, not the browser's. */
172 :root {
173 accent-color: var(--color-accent);
174 }
175}
176
177@media (prefers-reduced-motion: reduce) {
178 *,
179 *::before,
180 *::after {
181 animation: none !important;
182 scroll-behavior: auto !important;
183 }
184}
185
186/* Rendered markdown: READMEs, briefs and docs. */
187.prose > :first-child {
188 margin-top: 0;
189 padding-top: 0;
190}
191.prose {
192 @apply leading-7 text-fg;
193}
194.prose > * + * {
195 @apply mt-4;
196}
197.prose h1 {
198 @apply mt-0 text-3xl font-semibold tracking-tight;
199}
200.prose h2 {
201 @apply mt-10 border-b border-line pb-2 text-xl font-semibold tracking-tight;
202}
203.prose h3 {
204 @apply mt-8 text-base font-semibold;
205}
206.prose p,
207.prose li {
208 @apply text-[0.9375rem] text-fg/90;
209}
210.prose a {
211 @apply text-accent underline decoration-accent/40 underline-offset-4 hover:decoration-accent;
212}
213.prose ul {
214 @apply list-disc space-y-1.5 pl-6;
215}
216.prose ol {
217 @apply list-decimal space-y-1.5 pl-6;
218}
219.prose code {
220 @apply rounded bg-raised px-1.5 py-0.5 font-mono text-[0.85em];
221}
222.prose pre {
223 @apply overflow-x-auto rounded-lg border border-line bg-surface p-4 font-mono text-sm leading-6;
224}
225.prose pre code {
226 @apply bg-transparent p-0;
227}
228.prose blockquote {
229 @apply border-l-2 border-line-strong pl-4 text-muted;
230}
231.prose table {
232 @apply w-full border-collapse text-sm;
233}
234.prose th {
235 @apply border-b border-line-strong px-3 py-2 text-left font-medium text-muted;
236}
237.prose td {
238 @apply border-b border-line px-3 py-2 align-top;
239}
240.prose hr {
241 @apply border-line;
242}
243.prose h4 {
244 @apply mt-6 text-sm font-semibold;
245}
246.prose li.task-list-item {
247 @apply list-none;
248}
249.prose ul.contains-task-list {
250 @apply pl-1;
251}
252.prose kbd {
253 @apply rounded border border-line-strong bg-raised px-1.5 py-px font-mono text-[0.8em] shadow-[inset_0_-1px_0_var(--color-line-strong)];
254}
255.prose del {
256 @apply text-muted;
257}
258.prose details {
259 @apply rounded-lg border border-line px-4 py-2;
260}
261.prose summary {
262 @apply cursor-pointer font-medium;
263}
264.prose img {
265 @apply inline max-w-full;
266}
267.prose section[data-footnotes] {
268 @apply mt-10 border-t border-line pt-4 text-sm text-muted;
269}
270.prose a[data-footnote-ref] {
271 @apply no-underline;
272}
273
274/* Syntax-highlighted code from Shiki. */
275.shiki {
276 @apply bg-transparent! font-mono text-sm leading-6;
277}
278.shiki code {
279 counter-reset: line;
280 display: block;
281}
282.shiki .line::before {
283 counter-increment: line;
284 content: counter(line);
285 @apply mr-5 inline-block w-10 text-right text-faint select-none;
286}
287
288/*
289 * Artwork (components/art.tsx). Every piece loops on its drawing's
290 * `--dur` and starts at its own `--delay`, so the parts of one drawing
291 * stay in step. Only transforms, opacity and stroke offsets move. The
292 * resting style of each class is the finished picture: that is what
293 * prefers-reduced-motion shows, since the rule above stops every
294 * animation.
295 */
296.art-flow {
297 animation: dash 1.6s linear infinite;
298}
299
300/* Something travelling along a track (pathLength 100). Unseen at rest. */
301.art-packet {
302 opacity: 0;
303 stroke-dasharray: 4 200;
304 stroke-dashoffset: 4;
305 animation: art-travel var(--dur, 3s) cubic-bezier(0.45, 0, 0.35, 1) infinite;
306 animation-delay: var(--delay, 0s);
307}
308@keyframes art-travel {
309 0% {
310 stroke-dashoffset: 4;
311 opacity: 0;
312 }
313 3% {
314 opacity: 1;
315 }
316 32% {
317 opacity: 1;
318 }
319 36%,
320 100% {
321 stroke-dashoffset: -100;
322 opacity: 0;
323 }
324}
325
326/* A face that lights up for most of the loop. Lit at rest. */
327.art-lit {
328 opacity: 0.85;
329 animation: art-lit var(--dur, 8s) ease-in-out infinite both;
330 animation-delay: var(--delay, 0s);
331}
332@keyframes art-lit {
333 0%,
334 4% {
335 opacity: 0;
336 }
337 12%,
338 70% {
339 opacity: 0.9;
340 }
341 88%,
342 100% {
343 opacity: 0;
344 }
345}
346
347/* A short flash, as a check runs over a layer. Unseen at rest. */
348.art-scan {
349 opacity: 0;
350 animation: art-scan var(--dur, 8s) ease-out infinite both;
351 animation-delay: var(--delay, 0s);
352}
353@keyframes art-scan {
354 0% {
355 opacity: 0;
356 }
357 4% {
358 opacity: 1;
359 }
360 16%,
361 100% {
362 opacity: 0;
363 }
364}
365
366/* A line that draws itself, then fades. Drawn at rest. */
367.art-draw {
368 stroke-dasharray: 100;
369 stroke-dashoffset: 0;
370 animation: art-draw var(--dur, 8s) ease-in-out infinite both;
371 animation-delay: var(--delay, 0s);
372}
373@keyframes art-draw {
374 0% {
375 stroke-dashoffset: 100;
376 opacity: 1;
377 }
378 14%,
379 70% {
380 stroke-dashoffset: 0;
381 opacity: 1;
382 }
383 88%,
384 100% {
385 stroke-dashoffset: 0;
386 opacity: 0;
387 }
388}
389
390/* Something that appears, stays, and goes. Shown at rest. */
391.art-pop {
392 animation: art-pop var(--dur, 8s) ease-out infinite both;
393 animation-delay: var(--delay, 0s);
394}
395@keyframes art-pop {
396 0% {
397 opacity: 0;
398 transform: translateY(4px);
399 }
400 6%,
401 72% {
402 opacity: 1;
403 transform: none;
404 }
405 86%,
406 100% {
407 opacity: 0;
408 transform: none;
409 }
410}
411
412/* A layer dropping onto a stack. In place at rest. */
413.art-drop {
414 animation: art-drop var(--dur, 7s) cubic-bezier(0.2, 0.9, 0.3, 1.2) infinite both;
415 animation-delay: var(--delay, 0s);
416}
417@keyframes art-drop {
418 0% {
419 opacity: 0;
420 transform: translateY(-26px);
421 }
422 8%,
423 74% {
424 opacity: 1;
425 transform: none;
426 }
427 88%,
428 100% {
429 opacity: 0;
430 transform: none;
431 }
432}
433
434/* The layer that fails: drops, shakes, and is sent back. */
435.art-eject {
436 animation: art-eject var(--dur, 7s) ease-in-out infinite both;
437 animation-delay: var(--delay, 0s);
438}
439@keyframes art-eject {
440 0% {
441 opacity: 0;
442 transform: translateY(-26px);
443 }
444 8%,
445 30% {
446 opacity: 1;
447 transform: none;
448 }
449 33% {
450 transform: translateX(-3px);
451 }
452 36% {
453 transform: translateX(3px);
454 }
455 39% {
456 transform: translateX(-2px);
457 }
458 42% {
459 opacity: 1;
460 transform: none;
461 }
462 56%,
463 100% {
464 opacity: 0;
465 transform: translate(-40px, -22px);
466 }
467}
468
469/* A layer rising out of the one below it. In place at rest. */
470.art-rise {
471 animation: art-rise var(--dur, 7s) cubic-bezier(0.2, 0.8, 0.3, 1) infinite both;
472 animation-delay: var(--delay, 0s);
473}
474@keyframes art-rise {
475 0% {
476 opacity: 0;
477 transform: translateY(14px);
478 }
479 10%,
480 70% {
481 opacity: 1;
482 transform: none;
483 }
484 86%,
485 100% {
486 opacity: 0;
487 transform: none;
488 }
489}
490
491/* A soft glow breathing around a point. */
492.art-pulse {
493 transform-box: fill-box;
494 transform-origin: center;
495 animation: art-pulse var(--dur, 4s) ease-in-out infinite;
496 animation-delay: var(--delay, 0s);
497}
498@keyframes art-pulse {
499 0%,
500 100% {
501 transform: scale(0.8);
502 opacity: 1;
503 }
504 50% {
505 transform: scale(1.5);
506 opacity: 0.35;
507 }
508}
509
510/* Drawings off-screen hold still (components/art.tsx, Live). */
511[data-paused] * {
512 animation-play-state: paused !important;
513}
514/*
515 * Each drawing on its own layer, so a frame of its motion repaints only the
516 * drawing, not the page around it or the blurred header passing over it.
517 */
518[data-live] {
519 will-change: transform;
520}
521
522/* The landing page's steps, lit in turn along their line. */
523/* The ring is drawn once and only fades and grows, so the compositor runs it. */
524.flow-step::after {
525 content: "";
526 position: absolute;
527 inset: 0;
528 border-radius: inherit;
529 box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-merged) 18%, transparent);
530 opacity: 0;
531 pointer-events: none;
532 animation: flow-step 10s ease-in-out infinite both;
533 animation-delay: var(--delay, 0s);
534}
535@keyframes flow-step {
536 0%,
537 100% {
538 opacity: 0;
539 transform: scale(0.82);
540 }
541 6% {
542 opacity: 1;
543 transform: none;
544 }
545 16% {
546 opacity: 0;
547 transform: none;
548 }
549}
550.flow-runner {
551 animation: flow-runner 10s cubic-bezier(0.45, 0, 0.35, 1) infinite both;
552}
553@keyframes flow-runner {
554 0% {
555 transform: translateX(0);
556 opacity: 0;
557 }
558 4%,
559 76% {
560 opacity: 1;
561 }
562 80%,
563 100% {
564 transform: translateX(100%);
565 opacity: 0;
566 }
567}
568@media (prefers-reduced-motion: reduce) {
569 .flow-runner {
570 display: none;
571 }
572}
573
574/* The sidebar trading one far-side menu for another, in place. */
575@keyframes g1t-swap {
576 from {
577 opacity: 0;
578 transform: translateX(10px);
579 }
580 to {
581 opacity: 1;
582 transform: none;
583 }
584}
585
586/*
587 * Behind the landing page's opening: the same dark gray, with a faint
588 * lavender and peach glow low on the horizon and a few stars.
589 */
590.dusk {
591 background:
592 radial-gradient(1px 1px at 12% 18%, rgb(255 255 255 / 0.35), transparent),
593 radial-gradient(1px 1px at 27% 8%, rgb(255 255 255 / 0.25), transparent),
594 radial-gradient(1px 1px at 63% 12%, rgb(255 255 255 / 0.3), transparent),
595 radial-gradient(1.5px 1.5px at 90% 9%, rgb(255 255 255 / 0.35), transparent),
596 radial-gradient(60% 35% at 50% 100%, rgb(255 189 140 / 0.08), transparent 70%),
597 radial-gradient(80% 45% at 50% 100%, rgb(182 168 255 / 0.1), transparent 75%),
598 var(--color-bg);
599}
600/* A planet rising behind the horizon, its lit edge catching the glow. */
601.dusk-planet {
602 background: radial-gradient(circle at 70% 30%, rgb(182 168 255 / 0.12), rgb(255 189 140 / 0.04) 45%, transparent 70%);
603 box-shadow: inset 14px -8px 50px rgb(255 214 186 / 0.07);
604}