Skip to content

g1t/apps/web/app/app.css

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